|
52 | 52 | })) |
53 | 53 | } |
54 | 54 |
|
| 55 | +const WATCHED_FILE_PATTERNS: [&str; 6] = [ |
| 56 | + "**/biome.{json,jsonc}", |
| 57 | + "**/.biome.{json,jsonc}", |
| 58 | + ".editorconfig", |
| 59 | + "pnpm-workspace.yaml", |
| 60 | + "**/.gitignore", |
| 61 | + "**/.ignore", |
| 62 | +]; |
| 63 | + |
| 64 | +/// Escapes a filesystem path before embedding it in an LSP string glob. |
| 65 | +/// |
| 66 | +/// A [`RelativePattern`] keeps its base URI separate from its glob, but clients |
| 67 | +/// without relative-pattern support receive one string containing both. Glob |
| 68 | +/// metacharacters in the base path must therefore be represented as |
| 69 | +/// single-character ranges, and Windows separators must be converted to `/`. |
| 70 | +/// See the [LSP glob syntax] and [VS Code's escaping rules]. |
| 71 | +/// |
| 72 | +/// [LSP glob syntax]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#globPattern |
| 73 | +/// [VS Code's escaping rules]: https://code.visualstudio.com/docs/editor/glob-patterns#_common-questions |
| 74 | +fn escape_string_glob_base(path: &str) -> String { |
| 75 | + let mut escaped = String::with_capacity(path.len()); |
| 76 | + for char in path.chars() { |
| 77 | + match char { |
| 78 | + '\\' if cfg!(windows) => escaped.push('/'), |
| 79 | + '[' => escaped.push_str("[[]"), |
| 80 | + ']' => escaped.push_str("[]]"), |
| 81 | + '*' | '?' | '{' | '}' => { |
| 82 | + escaped.push('['); |
| 83 | + escaped.push(char); |
| 84 | + escaped.push(']'); |
| 85 | + } |
| 86 | + _ => escaped.push(char), |
| 87 | + } |
| 88 | + } |
| 89 | + escaped |
| 90 | +} |
| 91 | + |
| 92 | +fn watched_file_watchers( |
| 93 | + base_uri: OneOf<WorkspaceFolder, Uri>, |
| 94 | + relative_pattern_support: bool, |
| 95 | +) -> Option<Vec<FileSystemWatcher>> { |
| 96 | + if relative_pattern_support { |
| 97 | + return Some( |
| 98 | + WATCHED_FILE_PATTERNS |
| 99 | + .into_iter() |
| 100 | + .map(|pattern| FileSystemWatcher { |
| 101 | + glob_pattern: GlobPattern::Relative(RelativePattern { |
| 102 | + base_uri: base_uri.clone(), |
| 103 | + pattern: pattern.to_string(), |
| 104 | + }), |
| 105 | + kind: Some(WatchKind::all()), |
| 106 | + }) |
| 107 | + .collect(), |
| 108 | + ); |
| 109 | + } |
| 110 | + |
| 111 | + let uri = match &base_uri { |
| 112 | + OneOf::Left(folder) => &folder.uri, |
| 113 | + OneOf::Right(uri) => uri, |
| 114 | + }; |
| 115 | + if !uri.scheme().as_str().eq_ignore_ascii_case("file") { |
| 116 | + warn!("Unable to register file watchers for non-file URI {uri:?}."); |
| 117 | + return None; |
| 118 | + } |
| 119 | + let Some(path) = uri.to_file_path() else { |
| 120 | + warn!("Unable to convert {uri:?} to a file path for file watchers."); |
| 121 | + return None; |
| 122 | + }; |
| 123 | + let base_path = escape_string_glob_base(&path.to_string_lossy()); |
| 124 | + let separator = if base_path.ends_with('/') || base_path.ends_with('\\') { |
| 125 | + "" |
| 126 | + } else { |
| 127 | + "/" |
| 128 | + }; |
| 129 | + |
| 130 | + Some( |
| 131 | + WATCHED_FILE_PATTERNS |
| 132 | + .into_iter() |
| 133 | + .map(|pattern| FileSystemWatcher { |
| 134 | + glob_pattern: GlobPattern::String(format!("{base_path}{separator}{pattern}")), |
| 135 | + kind: Some(WatchKind::all()), |
| 136 | + }) |
| 137 | + .collect(), |
| 138 | + ) |
| 139 | +} |
| 140 | + |
55 | 141 | impl LSPServer { |
56 | 142 | fn new(session: SessionHandle, sessions: Sessions, lifecycle: Arc<SessionLifecycle>) -> Self { |
57 | 143 | Self { |
@@ -139,105 +225,30 @@ impl LSPServer { |
139 | 225 | ); |
140 | 226 |
|
141 | 227 | let watched_files_capability = if self.session.can_register_did_change_watched_files() { |
142 | | - if let Some(folders) = self.session.get_workspace_folders() { |
143 | | - let watchers = folders |
144 | | - .iter() |
145 | | - .flat_map(|folder| { |
146 | | - vec![ |
147 | | - FileSystemWatcher { |
148 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
149 | | - pattern: "**/biome.{json,jsonc}".to_string(), |
150 | | - base_uri: OneOf::Left(folder.clone()), |
151 | | - }), |
152 | | - kind: Some(WatchKind::all()), |
153 | | - }, |
154 | | - FileSystemWatcher { |
155 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
156 | | - pattern: "**/.biome.{json,jsonc}".to_string(), |
157 | | - base_uri: OneOf::Left(folder.clone()), |
158 | | - }), |
159 | | - kind: Some(WatchKind::all()), |
160 | | - }, |
161 | | - FileSystemWatcher { |
162 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
163 | | - pattern: ".editorconfig".to_string(), |
164 | | - base_uri: OneOf::Left(folder.clone()), |
165 | | - }), |
166 | | - kind: Some(WatchKind::all()), |
167 | | - }, |
168 | | - FileSystemWatcher { |
169 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
170 | | - pattern: "pnpm-workspace.yaml".to_string(), |
171 | | - base_uri: OneOf::Left(folder.clone()), |
172 | | - }), |
173 | | - kind: Some(WatchKind::all()), |
174 | | - }, |
175 | | - FileSystemWatcher { |
176 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
177 | | - pattern: "**/.gitignore".to_string(), |
178 | | - base_uri: OneOf::Left(folder.clone()), |
179 | | - }), |
180 | | - kind: Some(WatchKind::all()), |
181 | | - }, |
182 | | - FileSystemWatcher { |
183 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
184 | | - pattern: "**/.ignore".to_string(), |
185 | | - base_uri: OneOf::Left(folder.clone()), |
186 | | - }), |
187 | | - kind: Some(WatchKind::all()), |
188 | | - }, |
189 | | - ] |
190 | | - }) |
191 | | - .collect(); |
| 228 | + let base_uris: Vec<_> = match self.session.get_workspace_folders() { |
| 229 | + Some(folders) if !folders.is_empty() => { |
| 230 | + folders.into_iter().map(OneOf::Left).collect() |
| 231 | + } |
| 232 | + _ => self |
| 233 | + .session |
| 234 | + .root_uri() |
| 235 | + .map(OneOf::Right) |
| 236 | + .into_iter() |
| 237 | + .collect(), |
| 238 | + }; |
| 239 | + let relative_pattern_support = self.session.supports_relative_watched_file_patterns(); |
| 240 | + let watchers: Vec<_> = base_uris |
| 241 | + .into_iter() |
| 242 | + .filter_map(|base_uri| watched_file_watchers(base_uri, relative_pattern_support)) |
| 243 | + .flatten() |
| 244 | + .collect(); |
| 245 | + |
| 246 | + if watchers.is_empty() { |
| 247 | + CapabilityStatus::Disable |
| 248 | + } else { |
192 | 249 | CapabilityStatus::Enable(Some(json!(DidChangeWatchedFilesRegistrationOptions { |
193 | 250 | watchers |
194 | 251 | }))) |
195 | | - } else if let Some(base_uri) = self.session.base_uri() { |
196 | | - let base_path = self.session.base_path(); |
197 | | - let value = DidChangeWatchedFilesRegistrationOptions { |
198 | | - watchers: vec![ |
199 | | - FileSystemWatcher { |
200 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
201 | | - pattern: "**/biome.{json,jsonc}".to_string(), |
202 | | - base_uri: OneOf::Right(base_uri.clone()), |
203 | | - }), |
204 | | - kind: Some(WatchKind::all()), |
205 | | - }, |
206 | | - FileSystemWatcher { |
207 | | - glob_pattern: GlobPattern::Relative(RelativePattern { |
208 | | - pattern: "**/.biome.{json,jsonc}".to_string(), |
209 | | - base_uri: OneOf::Right(base_uri), |
210 | | - }), |
211 | | - kind: Some(WatchKind::all()), |
212 | | - }, |
213 | | - FileSystemWatcher { |
214 | | - glob_pattern: GlobPattern::String(base_path.as_ref().map_or_else( |
215 | | - || "**/.editorconfig".to_string(), |
216 | | - |p| format!("{}/.editorconfig", p.as_path().as_str()), |
217 | | - )), |
218 | | - kind: Some(WatchKind::all()), |
219 | | - }, |
220 | | - FileSystemWatcher { |
221 | | - glob_pattern: GlobPattern::String(base_path.as_ref().map_or_else( |
222 | | - || "**/pnpm-workspace.yaml".to_string(), |
223 | | - |p| format!("{}/pnpm-workspace.yaml", p.as_path().as_str()), |
224 | | - )), |
225 | | - kind: Some(WatchKind::all()), |
226 | | - }, |
227 | | - FileSystemWatcher { |
228 | | - glob_pattern: GlobPattern::String("**/.gitignore".to_string()), |
229 | | - kind: Some(WatchKind::all()), |
230 | | - }, |
231 | | - FileSystemWatcher { |
232 | | - glob_pattern: GlobPattern::String("**/.ignore".to_string()), |
233 | | - |
234 | | - kind: Some(WatchKind::all()), |
235 | | - }, |
236 | | - ], |
237 | | - }; |
238 | | - CapabilityStatus::Enable(Some(json!(value))) |
239 | | - } else { |
240 | | - CapabilityStatus::Disable |
241 | 252 | } |
242 | 253 | } else { |
243 | 254 | CapabilityStatus::Disable |
|
0 commit comments