Respect LSP servers watch glob patterns

This commit is contained in:
Max Brunsfeld 2023-03-23 15:27:43 -07:00
parent 361b7c3a0c
commit 3ff5aee4a1
5 changed files with 218 additions and 21 deletions

5
Cargo.lock generated
View file

@ -2591,9 +2591,9 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
@ -4633,6 +4633,7 @@ dependencies = [
"futures 0.3.25",
"fuzzy",
"git",
"glob",
"gpui",
"ignore",
"language",

View file

@ -27,6 +27,7 @@ fs = { path = "../fs" }
fsevent = { path = "../fsevent" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
glob = { version = "0.3.1" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }

View file

@ -0,0 +1,121 @@
use anyhow::{anyhow, Result};
use std::path::Path;
#[derive(Default)]
pub struct LspGlobSet {
patterns: Vec<glob::Pattern>,
}
impl LspGlobSet {
pub fn clear(&mut self) {
self.patterns.clear();
}
/// Add a pattern to the glob set.
///
/// LSP's glob syntax supports bash-style brace expansion. For example,
/// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
/// This is not a part of the standard libc glob syntax, and isn't supported
/// by the `glob` crate. So we pre-process the glob patterns, producing a
/// separate glob `Pattern` object for each part of a brace expansion.
pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
// Find all of the ranges of `pattern` that contain matched curly braces.
let mut expansion_ranges = Vec::new();
let mut expansion_start_ix = None;
for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
match c {
"{" => {
if expansion_start_ix.is_some() {
return Err(anyhow!("nested braces in glob patterns aren't supported"));
}
expansion_start_ix = Some(ix);
}
"}" => {
if let Some(start_ix) = expansion_start_ix {
expansion_ranges.push(start_ix..ix + 1);
}
expansion_start_ix = None;
}
_ => {}
}
}
// Starting with a single pattern, process each brace expansion by cloning
// the pattern once per element of the expansion.
let mut unexpanded_patterns = vec![];
let mut expanded_patterns = vec![pattern.to_string()];
for outer_range in expansion_ranges.into_iter().rev() {
let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
for unexpanded_pattern in unexpanded_patterns.drain(..) {
for part in unexpanded_pattern[inner_range.clone()].split(',') {
let mut expanded_pattern = unexpanded_pattern.clone();
expanded_pattern.replace_range(outer_range.clone(), part);
expanded_patterns.push(expanded_pattern);
}
}
}
// Parse the final glob patterns and add them to the set.
for pattern in expanded_patterns {
let pattern = glob::Pattern::new(&pattern)?;
self.patterns.push(pattern);
}
Ok(())
}
pub fn matches(&self, path: &Path) -> bool {
self.patterns
.iter()
.any(|pattern| pattern.matches_path(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_set() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/**/*.rs").unwrap();
watch.add_pattern("/a/**/Cargo.toml").unwrap();
assert!(watch.matches("/a/b.rs".as_ref()));
assert!(watch.matches("/a/b/c.rs".as_ref()));
assert!(!watch.matches("/b/c.rs".as_ref()));
assert!(!watch.matches("/a/b.ts".as_ref()));
}
#[test]
fn test_brace_expansion() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
assert!(watch.matches("/a/one.js".as_ref()));
assert!(watch.matches("/a/two.ts".as_ref()));
assert!(watch.matches("/a/three.tsx".as_ref()));
assert!(!watch.matches("/a/one.j".as_ref()));
assert!(!watch.matches("/a/two.s".as_ref()));
assert!(!watch.matches("/a/three.t".as_ref()));
assert!(!watch.matches("/a/four.t".as_ref()));
assert!(!watch.matches("/a/five.xt".as_ref()));
}
#[test]
fn test_multiple_brace_expansion() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
assert!(watch.matches("/a/one.bic".as_ref()));
assert!(watch.matches("/a/two.dole".as_ref()));
assert!(watch.matches("/a/three.deeee".as_ref()));
assert!(!watch.matches("/a/four.bic".as_ref()));
assert!(!watch.matches("/a/one.be".as_ref()));
}
}

View file

@ -1,5 +1,6 @@
mod ignore;
mod lsp_command;
mod lsp_glob_set;
pub mod search;
pub mod terminals;
pub mod worktree;
@ -33,10 +34,11 @@ use language::{
Transaction, Unclipped,
};
use lsp::{
DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
MarkedString,
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
DocumentHighlightKind, LanguageServer, LanguageString, MarkedString,
};
use lsp_command::*;
use lsp_glob_set::LspGlobSet;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
@ -188,6 +190,7 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
watched_paths: LspGlobSet,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@ -2046,8 +2049,26 @@ impl Project {
})
.detach();
language_server
.on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
Ok(())
.on_request::<lsp::request::RegisterCapability, _, _>({
let this = this.downgrade();
move |params, mut cx| async move {
let this = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project dropped"))?;
for reg in params.registrations {
if reg.method == "workspace/didChangeWatchedFiles" {
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
this.update(&mut cx, |this, cx| {
this.on_lsp_did_change_watched_files(
server_id, options, cx,
);
});
}
}
}
Ok(())
}
})
.detach();
@ -2117,6 +2138,7 @@ impl Project {
LanguageServerState::Running {
adapter: adapter.clone(),
language,
watched_paths: Default::default(),
server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
},
@ -2509,6 +2531,23 @@ impl Project {
}
}
fn on_lsp_did_change_watched_files(
&mut self,
language_server_id: usize,
params: DidChangeWatchedFilesRegistrationOptions,
cx: &mut ModelContext<Self>,
) {
if let Some(LanguageServerState::Running { watched_paths, .. }) =
self.language_servers.get_mut(&language_server_id)
{
watched_paths.clear();
for watcher in params.watchers {
watched_paths.add_pattern(&watcher.glob_pattern).log_err();
}
cx.notify();
}
}
async fn on_lsp_workspace_edit(
this: WeakModelHandle<Self>,
params: lsp::ApplyWorkspaceEditParams,
@ -4592,15 +4631,20 @@ impl Project {
for ((server_worktree_id, _), server_id) in &self.language_server_ids {
if *server_worktree_id == worktree_id {
if let Some(server) = self.language_servers.get(server_id) {
if let LanguageServerState::Running { server, .. } = server {
server
.notify::<lsp::notification::DidChangeWatchedFiles>(
lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
.map(|(path, change)| lsp::FileEvent {
uri: lsp::Url::from_file_path(abs_path.join(path))
.unwrap(),
if let LanguageServerState::Running {
server,
watched_paths,
..
} = server
{
let params = lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
.filter_map(|(path, change)| {
let path = abs_path.join(path);
if watched_paths.matches(&path) {
Some(lsp::FileEvent {
uri: lsp::Url::from_file_path(path).unwrap(),
typ: match change {
PathChange::Added => lsp::FileChangeType::CREATED,
PathChange::Removed => lsp::FileChangeType::DELETED,
@ -4610,10 +4654,18 @@ impl Project {
}
},
})
.collect(),
},
)
.log_err();
} else {
None
}
})
.collect(),
};
if !params.changes.is_empty() {
server
.notify::<lsp::notification::DidChangeWatchedFiles>(params)
.log_err();
}
}
}
}

View file

@ -493,6 +493,24 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
// Keep track of the FS events reported to the language server.
let fake_server = fake_servers.next().await.unwrap();
let file_changes = Arc::new(Mutex::new(Vec::new()));
fake_server
.request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
registrations: vec![lsp::Registration {
id: Default::default(),
method: "workspace/didChangeWatchedFiles".to_string(),
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
glob_pattern: "*.{rs,c}".to_string(),
kind: None,
}],
},
)
.ok(),
}],
})
.await
.unwrap();
fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
let file_changes = file_changes.clone();
move |params, _| {
@ -505,15 +523,19 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.foreground().run_until_parked();
assert_eq!(file_changes.lock().len(), 0);
// Perform some file system mutations.
// Perform some file system mutations, two of which match the watched patterns,
// and one of which does not.
fs.create_file("/the-root/c.rs".as_ref(), Default::default())
.await
.unwrap();
fs.create_file("/the-root/d.txt".as_ref(), Default::default())
.await
.unwrap();
fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
.await
.unwrap();
// The language server receives events for both FS mutations.
// The language server receives events for the FS mutations that match its watch patterns.
cx.foreground().run_until_parked();
assert_eq!(
&*file_changes.lock(),