From f561a91daf9a8d62bb3533cac2d0bf7842338d28 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:08:18 +0100 Subject: [PATCH] lsp: Add support for didRename/willRename LSP messages (#21651) Closes #21564 Notably, RA will now rename module references if you change the source file name via our project panel. This PR is a tad bigger than necessary as I torn out the Model<> from didSave watchers (I tried to reuse that code for the same purpose). Release Notes: - Added support for language server actions being executed on file rename. --- crates/lsp/src/lsp.rs | 6 + crates/project/src/lsp_store.rs | 392 ++++++++++++++++++++------- crates/project/src/project.rs | 42 ++- crates/project/src/project_tests.rs | 135 ++++++++- crates/project/src/worktree_store.rs | 68 ++++- 5 files changed, 537 insertions(+), 106 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 8789f5f252..4f714cccc9 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -638,6 +638,12 @@ impl LanguageServer { snippet_edit_support: Some(true), ..WorkspaceEditClientCapabilities::default() }), + file_operations: Some(WorkspaceFileOperationsClientCapabilities { + dynamic_registration: Some(false), + did_rename: Some(true), + will_rename: Some(true), + ..Default::default() + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ff2a3d47e7..6a9acd3048 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -23,10 +23,10 @@ use futures::{ stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, }; -use globset::{Glob, GlobSet, GlobSetBuilder}; +use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{ - AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel, - Task, WeakModel, + AppContext, AsyncAppContext, Entity, EventEmitter, Model, ModelContext, PromptLevel, Task, + WeakModel, }; use http_client::HttpClient; use itertools::Itertools as _; @@ -43,12 +43,13 @@ use language::{ Unclipped, }; use lsp::{ - CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, - DidChangeWatchedFilesRegistrationOptions, Edit, FileSystemWatcher, InsertTextFormat, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, - ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams, - WorkspaceFolder, + notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity, + DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, + FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, + InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, + RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, + WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, }; use node_runtime::read_package_installed_version; use parking_lot::{Mutex, RwLock}; @@ -139,7 +140,9 @@ pub struct LocalLspStore { pub language_servers: HashMap, buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, - language_server_watched_paths: HashMap>, + language_server_watched_paths: HashMap, + language_server_paths_watched_for_rename: + HashMap, language_server_watcher_registrations: HashMap>>, supplementary_language_servers: @@ -899,6 +902,7 @@ impl LspStore { language_servers: Default::default(), last_workspace_edits_by_language_server: Default::default(), language_server_watched_paths: Default::default(), + language_server_paths_watched_for_rename: Default::default(), language_server_watcher_registrations: Default::default(), current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), buffers_being_formatted: Default::default(), @@ -4332,6 +4336,112 @@ impl LspStore { .map(|(key, value)| (*key, value)) } + pub(super) fn did_rename_entry( + &self, + worktree_id: WorktreeId, + old_path: &Path, + new_path: &Path, + is_dir: bool, + ) { + maybe!({ + let local_store = self.as_local()?; + + let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from)?; + let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from)?; + + for language_server in self.language_servers_for_worktree(worktree_id) { + let Some(filter) = local_store + .language_server_paths_watched_for_rename + .get(&language_server.server_id()) + else { + continue; + }; + + if filter.should_send_did_rename(&old_uri, is_dir) { + language_server + .notify::(RenameFilesParams { + files: vec![FileRename { + old_uri: old_uri.clone(), + new_uri: new_uri.clone(), + }], + }) + .log_err(); + } + } + Some(()) + }); + } + + pub(super) fn will_rename_entry( + this: WeakModel, + worktree_id: WorktreeId, + old_path: &Path, + new_path: &Path, + is_dir: bool, + cx: AsyncAppContext, + ) -> Task<()> { + let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); + let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); + cx.spawn(move |mut cx| async move { + let mut tasks = vec![]; + this.update(&mut cx, |this, cx| { + let local_store = this.as_local()?; + let old_uri = old_uri?; + let new_uri = new_uri?; + for language_server in this.language_servers_for_worktree(worktree_id) { + let Some(filter) = local_store + .language_server_paths_watched_for_rename + .get(&language_server.server_id()) + else { + continue; + }; + let Some(adapter) = + this.language_server_adapter_for_id(language_server.server_id()) + else { + continue; + }; + if filter.should_send_will_rename(&old_uri, is_dir) { + let apply_edit = cx.spawn({ + let old_uri = old_uri.clone(); + let new_uri = new_uri.clone(); + let language_server = language_server.clone(); + |this, mut cx| async move { + let edit = language_server + .request::(RenameFilesParams { + files: vec![FileRename { old_uri, new_uri }], + }) + .log_err() + .await + .flatten()?; + + Self::deserialize_workspace_edit( + this.upgrade()?, + edit, + false, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .ok(); + Some(()) + } + }); + tasks.push(apply_edit); + } + } + Some(()) + }) + .ok() + .flatten(); + for task in tasks { + // Await on tasks sequentially so that the order of application of edits is deterministic + // (at least with regards to the order of registration of language servers) + task.await; + } + }) + } + fn lsp_notify_abs_paths_changed( &mut self, server_id: LanguageServerId, @@ -4369,6 +4479,32 @@ impl LspStore { language_server_id: LanguageServerId, cx: &mut ModelContext, ) { + let Some(watchers) = self.as_local().and_then(|local| { + local + .language_server_watcher_registrations + .get(&language_server_id) + }) else { + return; + }; + + let watch_builder = + self.rebuild_watched_paths_inner(language_server_id, watchers.values().flatten(), cx); + let Some(local_lsp_store) = self.as_local_mut() else { + return; + }; + let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx); + local_lsp_store + .language_server_watched_paths + .insert(language_server_id, watcher); + + cx.notify(); + } + fn rebuild_watched_paths_inner<'a>( + &'a self, + language_server_id: LanguageServerId, + watchers: impl Iterator, + cx: &mut ModelContext, + ) -> LanguageServerWatchedPathsBuilder { let worktrees = self .worktree_store .read(cx) @@ -4380,15 +4516,6 @@ impl LspStore { }) .collect::>(); - let local_lsp_store = self.as_local_mut().unwrap(); - - let Some(watchers) = local_lsp_store - .language_server_watcher_registrations - .get(&language_server_id) - else { - return; - }; - let mut worktree_globs = HashMap::default(); let mut abs_globs = HashMap::default(); log::trace!( @@ -4406,7 +4533,7 @@ impl LspStore { pattern: String, }, } - for watcher in watchers.values().flatten() { + for watcher in watchers { let mut found_host = false; for worktree in &worktrees { let glob_is_inside_worktree = worktree.update(cx, |tree, _| { @@ -4545,12 +4672,7 @@ impl LspStore { watch_builder.watch_abs_path(abs_path, globset); } } - let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx); - local_lsp_store - .language_server_watched_paths - .insert(language_server_id, watcher); - - cx.notify(); + watch_builder } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { @@ -6650,6 +6772,23 @@ impl LspStore { simulate_disk_based_diagnostics_completion: None, }, ); + if let Some(file_ops_caps) = language_server + .capabilities() + .workspace + .as_ref() + .and_then(|ws| ws.file_operations.as_ref()) + { + let did_rename_caps = file_ops_caps.did_rename.as_ref(); + let will_rename_caps = file_ops_caps.will_rename.as_ref(); + if did_rename_caps.or(will_rename_caps).is_some() { + let watcher = RenamePathsWatchedForServer::default() + .with_did_rename_patterns(did_rename_caps) + .with_will_rename_patterns(will_rename_caps); + local + .language_server_paths_watched_for_rename + .insert(server_id, watcher); + } + } } self.language_server_statuses.insert( @@ -7010,7 +7149,7 @@ impl LspStore { if let Some(watched_paths) = local .language_server_watched_paths .get(server_id) - .and_then(|paths| paths.read(cx).worktree_paths.get(&worktree_id)) + .and_then(|paths| paths.worktree_paths.get(&worktree_id)) { let params = lsp::DidChangeWatchedFilesParams { changes: changes @@ -7115,7 +7254,7 @@ impl LspStore { Ok(transaction) } - pub async fn deserialize_workspace_edit( + pub(crate) async fn deserialize_workspace_edit( this: Model, edit: lsp::WorkspaceEdit, push_to_history: bool, @@ -7515,6 +7654,84 @@ pub enum LanguageServerToQuery { Other(LanguageServerId), } +#[derive(Default)] +struct RenamePathsWatchedForServer { + did_rename: Vec, + will_rename: Vec, +} + +impl RenamePathsWatchedForServer { + fn with_did_rename_patterns( + mut self, + did_rename: Option<&FileOperationRegistrationOptions>, + ) -> Self { + if let Some(did_rename) = did_rename { + self.did_rename = did_rename + .filters + .iter() + .filter_map(|filter| filter.try_into().log_err()) + .collect(); + } + self + } + fn with_will_rename_patterns( + mut self, + will_rename: Option<&FileOperationRegistrationOptions>, + ) -> Self { + if let Some(will_rename) = will_rename { + self.will_rename = will_rename + .filters + .iter() + .filter_map(|filter| filter.try_into().log_err()) + .collect(); + } + self + } + + fn should_send_did_rename(&self, path: &str, is_dir: bool) -> bool { + self.did_rename.iter().any(|pred| pred.eval(path, is_dir)) + } + fn should_send_will_rename(&self, path: &str, is_dir: bool) -> bool { + self.will_rename.iter().any(|pred| pred.eval(path, is_dir)) + } +} + +impl TryFrom<&FileOperationFilter> for RenameActionPredicate { + type Error = globset::Error; + fn try_from(ops: &FileOperationFilter) -> Result { + Ok(Self { + kind: ops.pattern.matches.clone(), + glob: GlobBuilder::new(&ops.pattern.glob) + .case_insensitive( + ops.pattern + .options + .as_ref() + .map_or(false, |ops| ops.ignore_case.unwrap_or(false)), + ) + .build()? + .compile_matcher(), + }) + } +} +struct RenameActionPredicate { + glob: GlobMatcher, + kind: Option, +} + +impl RenameActionPredicate { + // Returns true if language server should be notified + fn eval(&self, path: &str, is_dir: bool) -> bool { + self.kind.as_ref().map_or(true, |kind| { + let expected_kind = if is_dir { + FileOperationPatternKind::Folder + } else { + FileOperationPatternKind::File + }; + kind == &expected_kind + }) && self.glob.is_match(path) + } +} + #[derive(Default)] struct LanguageServerWatchedPaths { worktree_paths: HashMap, @@ -7539,78 +7756,65 @@ impl LanguageServerWatchedPathsBuilder { fs: Arc, language_server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Model { + ) -> LanguageServerWatchedPaths { let project = cx.weak_model(); - cx.new_model(|cx| { - let this_id = cx.entity_id(); - const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); - let abs_paths = self - .abs_paths - .into_iter() - .map(|(abs_path, globset)| { - let task = cx.spawn({ - let abs_path = abs_path.clone(); - let fs = fs.clone(); + const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); + let abs_paths = self + .abs_paths + .into_iter() + .map(|(abs_path, globset)| { + let task = cx.spawn({ + let abs_path = abs_path.clone(); + let fs = fs.clone(); - let lsp_store = project.clone(); - |_, mut cx| async move { - maybe!(async move { - let mut push_updates = - fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; - while let Some(update) = push_updates.0.next().await { - let action = lsp_store - .update(&mut cx, |this, cx| { - let Some(local) = this.as_local() else { - return ControlFlow::Break(()); - }; - let Some(watcher) = local - .language_server_watched_paths - .get(&language_server_id) - else { - return ControlFlow::Break(()); - }; - if watcher.entity_id() != this_id { - // This watcher is no longer registered on the project, which means that we should - // cease operations. - return ControlFlow::Break(()); - } - let (globs, _) = watcher - .read(cx) - .abs_paths - .get(&abs_path) - .expect( - "Watched abs path is not registered with a watcher", - ); - let matching_entries = update - .into_iter() - .filter(|event| globs.is_match(&event.path)) - .collect::>(); - this.lsp_notify_abs_paths_changed( - language_server_id, - matching_entries, - ); - ControlFlow::Continue(()) - }) - .ok()?; + let lsp_store = project.clone(); + |_, mut cx| async move { + maybe!(async move { + let mut push_updates = fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; + while let Some(update) = push_updates.0.next().await { + let action = lsp_store + .update(&mut cx, |this, _| { + let Some(local) = this.as_local() else { + return ControlFlow::Break(()); + }; + let Some(watcher) = local + .language_server_watched_paths + .get(&language_server_id) + else { + return ControlFlow::Break(()); + }; + let (globs, _) = watcher.abs_paths.get(&abs_path).expect( + "Watched abs path is not registered with a watcher", + ); + let matching_entries = update + .into_iter() + .filter(|event| globs.is_match(&event.path)) + .collect::>(); + this.lsp_notify_abs_paths_changed( + language_server_id, + matching_entries, + ); + ControlFlow::Continue(()) + }) + .ok()?; - if action.is_break() { - break; - } - } - Some(()) - }) - .await; + if action.is_break() { + break; + } } - }); - (abs_path, (globset, task)) - }) - .collect(); - LanguageServerWatchedPaths { - worktree_paths: self.worktree_paths, - abs_paths, - } + Some(()) + }) + .await; + } + }); + (abs_path, (globset, task)) }) + .collect(); + LanguageServerWatchedPaths { + worktree_paths: self.worktree_paths, + abs_paths, + } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 84aedab92b..6ab800460e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -583,6 +583,8 @@ impl Project { client.add_model_request_handler(Self::handle_open_new_buffer); client.add_model_message_handler(Self::handle_create_buffer_for_peer); + client.add_model_request_handler(WorktreeStore::handle_rename_project_entry); + WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); @@ -1489,11 +1491,45 @@ impl Project { new_path: impl Into>, cx: &mut ModelContext, ) -> Task> { - let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + let worktree_store = self.worktree_store.read(cx); + let new_path = new_path.into(); + let Some((worktree, old_path, is_dir)) = worktree_store + .worktree_and_entry_for_id(entry_id, cx) + .map(|(worktree, entry)| (worktree, entry.path.clone(), entry.is_dir())) + else { return Task::ready(Err(anyhow!(format!("No worktree for entry {entry_id:?}")))); }; - worktree.update(cx, |worktree, cx| { - worktree.rename_entry(entry_id, new_path, cx) + + let worktree_id = worktree.read(cx).id(); + + let lsp_store = self.lsp_store().downgrade(); + cx.spawn(|_, mut cx| async move { + let (old_abs_path, new_abs_path) = { + let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; + (root_path.join(&old_path), root_path.join(&new_path)) + }; + LspStore::will_rename_entry( + lsp_store.clone(), + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + cx.clone(), + ) + .await; + + let entry = worktree + .update(&mut cx, |worktree, cx| { + worktree.rename_entry(entry_id, new_path.clone(), cx) + })? + .await?; + + lsp_store + .update(&mut cx, |this, _| { + this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); + }) + .ok(); + Ok(entry) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 26537503dc..0bd681a588 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9,12 +9,16 @@ use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; -use lsp::{DiagnosticSeverity, NumberOrString}; +use lsp::{ + notification::DidRenameFiles, DiagnosticSeverity, DocumentChanges, FileOperationFilter, + NumberOrString, TextDocumentEdit, WillRenameFiles, +}; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_matches}; use serde_json::json; #[cfg(not(windows))] use std::os; +use std::{str::FromStr, sync::OnceLock}; use std::{mem, num::NonZeroU32, ops::Range, task::Poll}; use task::{ResolvedTask, TaskContext}; @@ -3915,6 +3919,135 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two": { + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + } + + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let watched_paths = lsp::FileOperationRegistrationOptions { + filters: vec![ + FileOperationFilter { + scheme: Some("file".to_owned()), + pattern: lsp::FileOperationPattern { + glob: "**/*.rs".to_owned(), + matches: Some(lsp::FileOperationPatternKind::File), + options: None, + }, + }, + FileOperationFilter { + scheme: Some("file".to_owned()), + pattern: lsp::FileOperationPattern { + glob: "**/**".to_owned(), + matches: Some(lsp::FileOperationPatternKind::Folder), + options: None, + }, + }, + ], + }; + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + workspace: Some(lsp::WorkspaceServerCapabilities { + workspace_folders: None, + file_operations: Some(lsp::WorkspaceFileOperationsServerCapabilities { + did_rename: Some(watched_paths.clone()), + will_rename: Some(watched_paths), + ..Default::default() + }), + }), + ..Default::default() + }, + ..Default::default() + }, + ); + + let _ = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/one.rs", cx) + }) + .await + .unwrap(); + + let fake_server = fake_servers.next().await.unwrap(); + let response = project.update(cx, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + let entry = worktree.read(cx).entry_for_path("one.rs").unwrap(); + project.rename_entry(entry.id, "three.rs".as_ref(), cx) + }); + let expected_edit = lsp::WorkspaceEdit { + changes: None, + document_changes: Some(DocumentChanges::Edits({ + vec![TextDocumentEdit { + edits: vec![lsp::Edit::Plain(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 3, + }, + }, + new_text: "This is not a drill".to_owned(), + })], + text_document: lsp::OptionalVersionedTextDocumentIdentifier { + uri: Url::from_str("file:///dir/two/two.rs").unwrap(), + version: Some(1337), + }, + }] + })), + change_annotations: None, + }; + let resolved_workspace_edit = Arc::new(OnceLock::new()); + fake_server + .handle_request::({ + let resolved_workspace_edit = resolved_workspace_edit.clone(); + let expected_edit = expected_edit.clone(); + move |params, _| { + let resolved_workspace_edit = resolved_workspace_edit.clone(); + let expected_edit = expected_edit.clone(); + async move { + assert_eq!(params.files.len(), 1); + assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); + assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + resolved_workspace_edit.set(expected_edit.clone()).unwrap(); + Ok(Some(expected_edit)) + } + } + }) + .next() + .await + .unwrap(); + let _ = response.await.unwrap(); + fake_server + .handle_notification::(|params, _| { + assert_eq!(params.files.len(), 1); + assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); + assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + }) + .next() + .await + .unwrap(); + assert_eq!(resolved_workspace_edit.get(), Some(&expected_edit)); +} + #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { // hi diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 1e48cc052e..c39b88cd40 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -26,7 +26,7 @@ use text::ReplicaId; use util::{paths::SanitizedPath, ResultExt}; use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings}; -use crate::{search::SearchQuery, ProjectPath}; +use crate::{search::SearchQuery, LspStore, ProjectPath}; struct MatchingEntry { worktree_path: Arc, @@ -69,7 +69,6 @@ impl EventEmitter for WorktreeStore {} impl WorktreeStore { pub fn init(client: &AnyProtoClient) { client.add_model_request_handler(Self::handle_create_project_entry); - client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_expand_project_entry); @@ -184,6 +183,19 @@ impl WorktreeStore { .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id)) } + pub fn worktree_and_entry_for_id<'a>( + &'a self, + entry_id: ProjectEntryId, + cx: &'a AppContext, + ) -> Option<(Model, &'a Entry)> { + self.worktrees().find_map(|worktree| { + worktree + .read(cx) + .entry_for_id(entry_id) + .map(|e| (worktree.clone(), e)) + }) + } + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { self.worktree_for_id(path.worktree_id, cx)? .read(cx) @@ -1004,16 +1016,56 @@ impl WorktreeStore { } pub async fn handle_rename_project_entry( - this: Model, + this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> Result { let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); - let worktree = this.update(&mut cx, |this, cx| { - this.worktree_for_entry(entry_id, cx) - .ok_or_else(|| anyhow!("worktree not found")) - })??; - Worktree::handle_rename_entry(worktree, envelope.payload, cx).await + let (worktree_id, worktree, old_path, is_dir) = this + .update(&mut cx, |this, cx| { + this.worktree_store + .read(cx) + .worktree_and_entry_for_id(entry_id, cx) + .map(|(worktree, entry)| { + ( + worktree.read(cx).id(), + worktree, + entry.path.clone(), + entry.is_dir(), + ) + }) + })? + .ok_or_else(|| anyhow!("worktree not found"))?; + let (old_abs_path, new_abs_path) = { + let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; + ( + root_path.join(&old_path), + root_path.join(&envelope.payload.new_path), + ) + }; + let lsp_store = this + .update(&mut cx, |this, _| this.lsp_store())? + .downgrade(); + LspStore::will_rename_entry( + lsp_store, + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + cx.clone(), + ) + .await; + let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await; + this.update(&mut cx, |this, cx| { + this.lsp_store().read(cx).did_rename_entry( + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + ); + }) + .ok(); + response } pub async fn handle_copy_project_entry(