diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 81568b9e3e..ad94423e11 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -319,6 +319,9 @@ impl LanguageServer { capabilities: ClientCapabilities { workspace: Some(WorkspaceClientCapabilities { configuration: Some(true), + did_change_watched_files: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: Some(true), + }), did_change_configuration: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true), }), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5b2fd412e8..9e87b0b1bd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4465,7 +4465,10 @@ impl Project { cx.observe(worktree, |_, _, cx| cx.notify()).detach(); if worktree.read(cx).is_local() { cx.subscribe(worktree, |this, worktree, event, cx| match event { - worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx), + worktree::Event::UpdatedEntries(changes) => { + this.update_local_worktree_buffers(&worktree, cx); + this.update_local_worktree_language_servers(&worktree, changes, cx); + } worktree::Event::UpdatedGitRepositories(updated_repos) => { this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) } @@ -4496,7 +4499,7 @@ impl Project { fn update_local_worktree_buffers( &mut self, - worktree_handle: ModelHandle, + worktree_handle: &ModelHandle, cx: &mut ModelContext, ) { let snapshot = worktree_handle.read(cx).snapshot(); @@ -4506,7 +4509,7 @@ impl Project { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| { if let Some(old_file) = File::from_dyn(buffer.file()) { - if old_file.worktree != worktree_handle { + if old_file.worktree != *worktree_handle { return; } @@ -4578,6 +4581,42 @@ impl Project { } } + fn update_local_worktree_language_servers( + &mut self, + worktree_handle: &ModelHandle, + changes: &HashMap, PathChange>, + cx: &mut ModelContext, + ) { + let worktree_id = worktree_handle.read(cx).id(); + let abs_path = worktree_handle.read(cx).abs_path(); + 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::DidChangeWatchedFilesParams { + changes: changes + .iter() + .map(|(path, change)| lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)) + .unwrap(), + typ: match change { + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + }, + }) + .collect(), + }, + ) + .log_err(); + } + } + } + } + } + fn update_local_worktree_buffers_git_repos( &mut self, worktree: ModelHandle, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index c0b72cf5b6..61f207c45a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -509,14 +509,14 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon assert_eq!( &*file_changes.lock(), &[ + lsp::FileEvent { + uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(), + typ: lsp::FileChangeType::DELETED, + }, lsp::FileEvent { uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(), typ: lsp::FileChangeType::CREATED, }, - lsp::FileEvent { - uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(), - typ: lsp::FileChangeType::DELETED, - } ] ); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 59a98bdcfa..f0bf81e49e 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -33,7 +33,6 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; - use smol::channel::{self, Sender}; use std::{ any::Any, @@ -65,6 +64,7 @@ pub enum Worktree { pub struct LocalWorktree { snapshot: LocalSnapshot, background_snapshot: Arc>, + background_changes: Arc, PathChange>>>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, poll_task: Option>, @@ -175,7 +175,7 @@ struct ShareState { } pub enum Event { - UpdatedEntries, + UpdatedEntries(HashMap, PathChange>), UpdatedGitRepositories(Vec), } @@ -198,11 +198,17 @@ impl Worktree { let tree = tree.as_local_mut().unwrap(); let abs_path = tree.abs_path().clone(); let background_snapshot = tree.background_snapshot.clone(); + let background_changes = tree.background_changes.clone(); let background = cx.background().clone(); tree._background_scanner_task = Some(cx.background().spawn(async move { let events = fs.watch(&abs_path, Duration::from_millis(100)).await; - let scanner = - BackgroundScanner::new(background_snapshot, scan_states_tx, fs, background); + let scanner = BackgroundScanner::new( + background_snapshot, + background_changes, + scan_states_tx, + fs, + background, + ); scanner.run(events).await; })); }); @@ -451,6 +457,7 @@ impl LocalWorktree { let tree = Self { snapshot: snapshot.clone(), background_snapshot: Arc::new(Mutex::new(snapshot)), + background_changes: Arc::new(Mutex::new(HashMap::default())), last_scan_state_rx, _background_scanner_task: None, share: None, @@ -563,6 +570,7 @@ impl LocalWorktree { match self.scan_state() { ScanState::Idle => { let new_snapshot = self.background_snapshot.lock().clone(); + let changes = mem::take(&mut *self.background_changes.lock()); let updated_repos = Self::changed_repos( &self.snapshot.git_repositories, &new_snapshot.git_repositories, @@ -573,7 +581,7 @@ impl LocalWorktree { *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); } - cx.emit(Event::UpdatedEntries); + cx.emit(Event::UpdatedEntries(changes)); if !updated_repos.is_empty() { cx.emit(Event::UpdatedGitRepositories(updated_repos)); @@ -602,7 +610,7 @@ impl LocalWorktree { } })); - cx.emit(Event::UpdatedEntries); + cx.emit(Event::UpdatedEntries(Default::default())); if !updated_repos.is_empty() { cx.emit(Event::UpdatedGitRepositories(updated_repos)); @@ -994,15 +1002,26 @@ impl LocalWorktree { let inserted_entry; { let mut snapshot = this.background_snapshot.lock(); + let mut changes = this.background_changes.lock(); let mut entry = Entry::new(path, &metadata, &next_entry_id, root_char_bag); entry.is_ignored = snapshot .ignore_stack_for_abs_path(&abs_path, entry.is_dir()) .is_abs_path_ignored(&abs_path, entry.is_dir()); if let Some(old_path) = old_path { snapshot.remove_path(&old_path); + changes.insert(old_path.clone(), PathChange::Removed); } snapshot.scan_started(); + let exists = snapshot.entry_for_path(&entry.path).is_some(); inserted_entry = snapshot.insert_entry(entry, fs.as_ref()); + changes.insert( + inserted_entry.path.clone(), + if exists { + PathChange::Updated + } else { + PathChange::Added + }, + ); snapshot.scan_completed(); } this.poll_snapshot(true, cx); @@ -1111,7 +1130,7 @@ impl RemoteWorktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { self.snapshot = self.background_snapshot.lock().clone(); - cx.emit(Event::UpdatedEntries); + cx.emit(Event::UpdatedEntries(Default::default())); cx.notify(); } @@ -2048,6 +2067,13 @@ pub enum EntryKind { File(CharBag), } +#[derive(Clone, Copy, Debug)] +pub enum PathChange { + Added, + Removed, + Updated, +} + impl Entry { fn new( path: Arc, @@ -2206,6 +2232,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { struct BackgroundScanner { fs: Arc, snapshot: Arc>, + changes: Arc, PathChange>>>, notify: UnboundedSender, executor: Arc, } @@ -2213,6 +2240,7 @@ struct BackgroundScanner { impl BackgroundScanner { fn new( snapshot: Arc>, + changes: Arc, PathChange>>>, notify: UnboundedSender, fs: Arc, executor: Arc, @@ -2220,6 +2248,7 @@ impl BackgroundScanner { Self { fs, snapshot, + changes, notify, executor, } @@ -2486,12 +2515,14 @@ impl BackgroundScanner { let root_char_bag; let root_abs_path; let next_entry_id; + let prev_snapshot; { let mut snapshot = self.snapshot.lock(); - snapshot.scan_started(); + prev_snapshot = snapshot.snapshot.clone(); root_char_bag = snapshot.root_char_bag; root_abs_path = snapshot.abs_path.clone(); next_entry_id = snapshot.next_entry_id.clone(); + snapshot.scan_started(); } let root_canonical_path = if let Ok(path) = self.fs.canonicalize(&root_abs_path).await { @@ -2510,6 +2541,7 @@ impl BackgroundScanner { // Hold the snapshot lock while clearing and re-inserting the root entries // for each event. This way, the snapshot is not observable to the foreground // thread while this operation is in-progress. + let mut event_paths = Vec::with_capacity(events.len()); let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); { let mut snapshot = self.snapshot.lock(); @@ -2531,6 +2563,7 @@ impl BackgroundScanner { continue; } }; + event_paths.push(path.clone()); let abs_path = root_abs_path.join(&path); match metadata { @@ -2599,6 +2632,7 @@ impl BackgroundScanner { self.update_ignore_statuses().await; self.update_git_repositories(); + self.build_change_set(prev_snapshot, event_paths); self.snapshot.lock().scan_completed(); true } @@ -2714,6 +2748,60 @@ impl BackgroundScanner { snapshot.entries_by_path.edit(entries_by_path_edits, &()); snapshot.entries_by_id.edit(entries_by_id_edits, &()); } + + fn build_change_set(&self, old_snapshot: Snapshot, event_paths: Vec>) { + let new_snapshot = self.snapshot.lock(); + let mut old_paths = old_snapshot.entries_by_path.cursor::(); + let mut new_paths = new_snapshot.entries_by_path.cursor::(); + + let mut change_set = self.changes.lock(); + for path in event_paths { + let path = PathKey(path); + old_paths.seek(&path, Bias::Left, &()); + new_paths.seek(&path, Bias::Left, &()); + + loop { + match (old_paths.item(), new_paths.item()) { + (Some(old_entry), Some(new_entry)) => { + if old_entry.path > path.0 + && new_entry.path > path.0 + && !old_entry.path.starts_with(&path.0) + && !new_entry.path.starts_with(&path.0) + { + break; + } + + match Ord::cmp(&old_entry.path, &new_entry.path) { + Ordering::Less => { + change_set.insert(old_entry.path.clone(), PathChange::Removed); + old_paths.next(&()); + } + Ordering::Equal => { + if old_entry.mtime != new_entry.mtime { + change_set.insert(old_entry.path.clone(), PathChange::Updated); + } + old_paths.next(&()); + new_paths.next(&()); + } + Ordering::Greater => { + change_set.insert(new_entry.path.clone(), PathChange::Added); + new_paths.next(&()); + } + } + } + (Some(old_entry), None) => { + change_set.insert(old_entry.path.clone(), PathChange::Removed); + old_paths.next(&()); + } + (None, Some(new_entry)) => { + change_set.insert(new_entry.path.clone(), PathChange::Added); + new_paths.next(&()); + } + (None, None) => break, + } + } + } + } } fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { @@ -3500,6 +3588,7 @@ mod tests { ); let mut scanner = BackgroundScanner::new( Arc::new(Mutex::new(initial_snapshot.clone())), + Arc::new(Mutex::new(HashMap::default())), notify_tx, fs.clone(), Arc::new(gpui::executor::Background::new()), @@ -3533,6 +3622,7 @@ mod tests { let (notify_tx, _notify_rx) = mpsc::unbounded(); let mut new_scanner = BackgroundScanner::new( Arc::new(Mutex::new(initial_snapshot)), + Arc::new(Mutex::new(HashMap::default())), notify_tx, scanner.fs.clone(), scanner.executor.clone(),