solidity_language_server/
utils.rs1use std::{
2 path::{Path, PathBuf},
3 sync::OnceLock,
4};
5use tower_lsp::lsp_types::PositionEncodingKind;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PositionEncoding {
14 Utf8,
16 Utf16,
19}
20
21impl PositionEncoding {
22 pub const DEFAULT: Self = PositionEncoding::Utf16;
24
25 pub fn negotiate(client_encodings: Option<&[PositionEncodingKind]>) -> Self {
29 let Some(encodings) = client_encodings else {
30 return Self::DEFAULT;
31 };
32 if encodings.contains(&PositionEncodingKind::UTF8) {
33 PositionEncoding::Utf8
34 } else {
35 PositionEncoding::Utf16
36 }
37 }
38
39 pub fn to_encoding_kind(self) -> PositionEncodingKind {
41 match self {
42 PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
43 PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
44 }
45 }
46}
47
48static ENCODING: OnceLock<PositionEncoding> = OnceLock::new();
53
54pub fn set_encoding(enc: PositionEncoding) {
57 let _ = ENCODING.set(enc);
58}
59
60pub fn encoding() -> PositionEncoding {
62 ENCODING.get().copied().unwrap_or(PositionEncoding::DEFAULT)
63}
64
65pub fn byte_offset_to_position(source: &str, byte_offset: usize) -> (u32, u32) {
72 let enc = encoding();
73 let mut line: u32 = 0;
74 let mut col: u32 = 0;
75 let bytes = source.as_bytes();
76 let mut i = 0;
77
78 while i < byte_offset && i < bytes.len() {
79 match bytes[i] {
80 b'\n' => {
81 line += 1;
82 col = 0;
83 i += 1;
84 }
85 b'\r' if i + 1 < bytes.len() && bytes[i + 1] == b'\n' => {
86 line += 1;
87 col = 0;
88 i += 2;
89 }
90 _ => {
91 match enc {
92 PositionEncoding::Utf8 => {
93 col += 1;
95 i += 1;
96 }
97 PositionEncoding::Utf16 => {
98 let ch_len = utf8_char_len(bytes[i]);
100 let ch = &source[i..i + ch_len];
101 col += ch.chars().next().map(|c| c.len_utf16() as u32).unwrap_or(1);
102 i += ch_len;
103 }
104 }
105 }
106 }
107 }
108
109 (line, col)
110}
111
112pub fn position_to_byte_offset(source: &str, line: u32, character: u32) -> usize {
115 let enc = encoding();
116 let mut current_line: u32 = 0;
117 let mut current_col: u32 = 0;
118
119 for (i, ch) in source.char_indices() {
120 if current_line == line && current_col == character {
121 return i;
122 }
123
124 match ch {
125 '\n' => {
126 if current_line == line {
127 return i; }
129 current_line += 1;
130 current_col = 0;
131 }
132 _ => {
133 current_col += match enc {
134 PositionEncoding::Utf8 => ch.len_utf8() as u32,
135 PositionEncoding::Utf16 => ch.len_utf16() as u32,
136 };
137 }
138 }
139 }
140
141 source.len()
142}
143
144fn utf8_char_len(lead: u8) -> usize {
150 match lead {
151 0x00..=0x7F => 1,
152 0xC0..=0xDF => 2,
153 0xE0..=0xEF => 3,
154 0xF0..=0xF7 => 4,
155 _ => 1, }
157}
158
159pub fn is_valid_solidity_identifier(name: &str) -> bool {
160 if name.is_empty() {
161 return false;
162 }
163 let chars: Vec<char> = name.chars().collect();
164 let first = chars[0];
165 if !first.is_ascii_alphabetic() && first != '_' {
166 return false;
167 }
168 for &c in &chars {
169 if !c.is_ascii_alphanumeric() && c != '_' {
170 return false;
171 }
172 }
173 true
174}
175
176pub fn find_git_root(path: impl AsRef<Path>) -> Option<PathBuf> {
178 path.as_ref()
179 .ancestors()
180 .find(|p| p.join(".git").exists())
181 .map(Path::to_path_buf)
182}
183
184pub fn find_project_root(path: impl AsRef<Path>) -> Option<PathBuf> {
187 let path = path.as_ref();
188 let boundary = find_git_root(path);
189 let found = path
190 .ancestors()
191 .take_while(|p| {
192 if let Some(boundary) = &boundary {
193 p.starts_with(boundary)
194 } else {
195 true
196 }
197 })
198 .find(|p| p.join("foundry.toml").is_file())
199 .map(Path::to_path_buf);
200 found.or(boundary)
201}