mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-27 12:54:42 +00:00
Respect LSP servers watch glob patterns
This commit is contained in:
parent
361b7c3a0c
commit
3ff5aee4a1
5 changed files with 218 additions and 21 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
121
crates/project/src/lsp_glob_set.rs
Normal file
121
crates/project/src/lsp_glob_set.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in a new issue