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.
This commit is contained in:
parent
14ba4a9c94
commit
f561a91daf
5 changed files with 537 additions and 106 deletions
|
@ -638,6 +638,12 @@ impl LanguageServer {
|
||||||
snippet_edit_support: Some(true),
|
snippet_edit_support: Some(true),
|
||||||
..WorkspaceEditClientCapabilities::default()
|
..WorkspaceEditClientCapabilities::default()
|
||||||
}),
|
}),
|
||||||
|
file_operations: Some(WorkspaceFileOperationsClientCapabilities {
|
||||||
|
dynamic_registration: Some(false),
|
||||||
|
did_rename: Some(true),
|
||||||
|
will_rename: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
text_document: Some(TextDocumentClientCapabilities {
|
text_document: Some(TextDocumentClientCapabilities {
|
||||||
|
|
|
@ -23,10 +23,10 @@ use futures::{
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel,
|
AppContext, AsyncAppContext, Entity, EventEmitter, Model, ModelContext, PromptLevel, Task,
|
||||||
Task, WeakModel,
|
WeakModel,
|
||||||
};
|
};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
|
@ -43,12 +43,13 @@ use language::{
|
||||||
Unclipped,
|
Unclipped,
|
||||||
};
|
};
|
||||||
use lsp::{
|
use lsp::{
|
||||||
CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag,
|
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
|
||||||
DidChangeWatchedFilesRegistrationOptions, Edit, FileSystemWatcher, InsertTextFormat,
|
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
|
||||||
LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId,
|
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
|
||||||
LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
|
InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
|
||||||
ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams,
|
LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
|
||||||
WorkspaceFolder,
|
RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url,
|
||||||
|
WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
|
||||||
};
|
};
|
||||||
use node_runtime::read_package_installed_version;
|
use node_runtime::read_package_installed_version;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
@ -139,7 +140,9 @@ pub struct LocalLspStore {
|
||||||
pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
|
pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
|
||||||
buffers_being_formatted: HashSet<BufferId>,
|
buffers_being_formatted: HashSet<BufferId>,
|
||||||
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
|
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
|
||||||
language_server_watched_paths: HashMap<LanguageServerId, Model<LanguageServerWatchedPaths>>,
|
language_server_watched_paths: HashMap<LanguageServerId, LanguageServerWatchedPaths>,
|
||||||
|
language_server_paths_watched_for_rename:
|
||||||
|
HashMap<LanguageServerId, RenamePathsWatchedForServer>,
|
||||||
language_server_watcher_registrations:
|
language_server_watcher_registrations:
|
||||||
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
|
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
|
||||||
supplementary_language_servers:
|
supplementary_language_servers:
|
||||||
|
@ -899,6 +902,7 @@ impl LspStore {
|
||||||
language_servers: Default::default(),
|
language_servers: Default::default(),
|
||||||
last_workspace_edits_by_language_server: Default::default(),
|
last_workspace_edits_by_language_server: Default::default(),
|
||||||
language_server_watched_paths: Default::default(),
|
language_server_watched_paths: Default::default(),
|
||||||
|
language_server_paths_watched_for_rename: Default::default(),
|
||||||
language_server_watcher_registrations: Default::default(),
|
language_server_watcher_registrations: Default::default(),
|
||||||
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
||||||
buffers_being_formatted: Default::default(),
|
buffers_being_formatted: Default::default(),
|
||||||
|
@ -4332,6 +4336,112 @@ impl LspStore {
|
||||||
.map(|(key, value)| (*key, value))
|
.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::<DidRenameFiles>(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<Self>,
|
||||||
|
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::<WillRenameFiles>(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(
|
fn lsp_notify_abs_paths_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
|
@ -4369,6 +4479,32 @@ impl LspStore {
|
||||||
language_server_id: LanguageServerId,
|
language_server_id: LanguageServerId,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
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<Item = &'a FileSystemWatcher>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> LanguageServerWatchedPathsBuilder {
|
||||||
let worktrees = self
|
let worktrees = self
|
||||||
.worktree_store
|
.worktree_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -4380,15 +4516,6 @@ impl LspStore {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
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 worktree_globs = HashMap::default();
|
||||||
let mut abs_globs = HashMap::default();
|
let mut abs_globs = HashMap::default();
|
||||||
log::trace!(
|
log::trace!(
|
||||||
|
@ -4406,7 +4533,7 @@ impl LspStore {
|
||||||
pattern: String,
|
pattern: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for watcher in watchers.values().flatten() {
|
for watcher in watchers {
|
||||||
let mut found_host = false;
|
let mut found_host = false;
|
||||||
for worktree in &worktrees {
|
for worktree in &worktrees {
|
||||||
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
|
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
|
||||||
|
@ -4545,12 +4672,7 @@ impl LspStore {
|
||||||
watch_builder.watch_abs_path(abs_path, globset);
|
watch_builder.watch_abs_path(abs_path, globset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx);
|
watch_builder
|
||||||
local_lsp_store
|
|
||||||
.language_server_watched_paths
|
|
||||||
.insert(language_server_id, watcher);
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
|
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
|
||||||
|
@ -6650,6 +6772,23 @@ impl LspStore {
|
||||||
simulate_disk_based_diagnostics_completion: None,
|
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(
|
self.language_server_statuses.insert(
|
||||||
|
@ -7010,7 +7149,7 @@ impl LspStore {
|
||||||
if let Some(watched_paths) = local
|
if let Some(watched_paths) = local
|
||||||
.language_server_watched_paths
|
.language_server_watched_paths
|
||||||
.get(server_id)
|
.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 {
|
let params = lsp::DidChangeWatchedFilesParams {
|
||||||
changes: changes
|
changes: changes
|
||||||
|
@ -7115,7 +7254,7 @@ impl LspStore {
|
||||||
Ok(transaction)
|
Ok(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn deserialize_workspace_edit(
|
pub(crate) async fn deserialize_workspace_edit(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
edit: lsp::WorkspaceEdit,
|
edit: lsp::WorkspaceEdit,
|
||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
|
@ -7515,6 +7654,84 @@ pub enum LanguageServerToQuery {
|
||||||
Other(LanguageServerId),
|
Other(LanguageServerId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RenamePathsWatchedForServer {
|
||||||
|
did_rename: Vec<RenameActionPredicate>,
|
||||||
|
will_rename: Vec<RenameActionPredicate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, globset::Error> {
|
||||||
|
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<FileOperationPatternKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Default)]
|
||||||
struct LanguageServerWatchedPaths {
|
struct LanguageServerWatchedPaths {
|
||||||
worktree_paths: HashMap<WorktreeId, GlobSet>,
|
worktree_paths: HashMap<WorktreeId, GlobSet>,
|
||||||
|
@ -7539,78 +7756,65 @@ impl LanguageServerWatchedPathsBuilder {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_server_id: LanguageServerId,
|
language_server_id: LanguageServerId,
|
||||||
cx: &mut ModelContext<LspStore>,
|
cx: &mut ModelContext<LspStore>,
|
||||||
) -> Model<LanguageServerWatchedPaths> {
|
) -> LanguageServerWatchedPaths {
|
||||||
let project = cx.weak_model();
|
let project = cx.weak_model();
|
||||||
|
|
||||||
cx.new_model(|cx| {
|
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
|
||||||
let this_id = cx.entity_id();
|
let abs_paths = self
|
||||||
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
|
.abs_paths
|
||||||
let abs_paths = self
|
.into_iter()
|
||||||
.abs_paths
|
.map(|(abs_path, globset)| {
|
||||||
.into_iter()
|
let task = cx.spawn({
|
||||||
.map(|(abs_path, globset)| {
|
let abs_path = abs_path.clone();
|
||||||
let task = cx.spawn({
|
let fs = fs.clone();
|
||||||
let abs_path = abs_path.clone();
|
|
||||||
let fs = fs.clone();
|
|
||||||
|
|
||||||
let lsp_store = project.clone();
|
let lsp_store = project.clone();
|
||||||
|_, mut cx| async move {
|
|_, mut cx| async move {
|
||||||
maybe!(async move {
|
maybe!(async move {
|
||||||
let mut push_updates =
|
let mut push_updates = fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
|
||||||
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
|
while let Some(update) = push_updates.0.next().await {
|
||||||
while let Some(update) = push_updates.0.next().await {
|
let action = lsp_store
|
||||||
let action = lsp_store
|
.update(&mut cx, |this, _| {
|
||||||
.update(&mut cx, |this, cx| {
|
let Some(local) = this.as_local() else {
|
||||||
let Some(local) = this.as_local() else {
|
return ControlFlow::Break(());
|
||||||
return ControlFlow::Break(());
|
};
|
||||||
};
|
let Some(watcher) = local
|
||||||
let Some(watcher) = local
|
.language_server_watched_paths
|
||||||
.language_server_watched_paths
|
.get(&language_server_id)
|
||||||
.get(&language_server_id)
|
else {
|
||||||
else {
|
return ControlFlow::Break(());
|
||||||
return ControlFlow::Break(());
|
};
|
||||||
};
|
let (globs, _) = watcher.abs_paths.get(&abs_path).expect(
|
||||||
if watcher.entity_id() != this_id {
|
"Watched abs path is not registered with a watcher",
|
||||||
// This watcher is no longer registered on the project, which means that we should
|
);
|
||||||
// cease operations.
|
let matching_entries = update
|
||||||
return ControlFlow::Break(());
|
.into_iter()
|
||||||
}
|
.filter(|event| globs.is_match(&event.path))
|
||||||
let (globs, _) = watcher
|
.collect::<Vec<_>>();
|
||||||
.read(cx)
|
this.lsp_notify_abs_paths_changed(
|
||||||
.abs_paths
|
language_server_id,
|
||||||
.get(&abs_path)
|
matching_entries,
|
||||||
.expect(
|
);
|
||||||
"Watched abs path is not registered with a watcher",
|
ControlFlow::Continue(())
|
||||||
);
|
})
|
||||||
let matching_entries = update
|
.ok()?;
|
||||||
.into_iter()
|
|
||||||
.filter(|event| globs.is_match(&event.path))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
this.lsp_notify_abs_paths_changed(
|
|
||||||
language_server_id,
|
|
||||||
matching_entries,
|
|
||||||
);
|
|
||||||
ControlFlow::Continue(())
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
if action.is_break() {
|
if action.is_break() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Some(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
});
|
Some(())
|
||||||
(abs_path, (globset, task))
|
})
|
||||||
})
|
.await;
|
||||||
.collect();
|
}
|
||||||
LanguageServerWatchedPaths {
|
});
|
||||||
worktree_paths: self.worktree_paths,
|
(abs_path, (globset, task))
|
||||||
abs_paths,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.collect();
|
||||||
|
LanguageServerWatchedPaths {
|
||||||
|
worktree_paths: self.worktree_paths,
|
||||||
|
abs_paths,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -583,6 +583,8 @@ impl Project {
|
||||||
client.add_model_request_handler(Self::handle_open_new_buffer);
|
client.add_model_request_handler(Self::handle_open_new_buffer);
|
||||||
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
|
||||||
|
|
||||||
|
client.add_model_request_handler(WorktreeStore::handle_rename_project_entry);
|
||||||
|
|
||||||
WorktreeStore::init(&client);
|
WorktreeStore::init(&client);
|
||||||
BufferStore::init(&client);
|
BufferStore::init(&client);
|
||||||
LspStore::init(&client);
|
LspStore::init(&client);
|
||||||
|
@ -1489,11 +1491,45 @@ impl Project {
|
||||||
new_path: impl Into<Arc<Path>>,
|
new_path: impl Into<Arc<Path>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<CreatedEntry>> {
|
) -> Task<Result<CreatedEntry>> {
|
||||||
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:?}"))));
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,16 @@ use language::{
|
||||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter,
|
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter,
|
||||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
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 parking_lot::Mutex;
|
||||||
use pretty_assertions::{assert_eq, assert_matches};
|
use pretty_assertions::{assert_eq, assert_matches};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::os;
|
use std::os;
|
||||||
|
use std::{str::FromStr, sync::OnceLock};
|
||||||
|
|
||||||
use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
|
use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
|
||||||
use task::{ResolvedTask, TaskContext};
|
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::<WillRenameFiles, _, _>({
|
||||||
|
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::<DidRenameFiles, _>(|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]
|
#[gpui::test]
|
||||||
async fn test_rename(cx: &mut gpui::TestAppContext) {
|
async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||||
// hi
|
// hi
|
||||||
|
|
|
@ -26,7 +26,7 @@ use text::ReplicaId;
|
||||||
use util::{paths::SanitizedPath, ResultExt};
|
use util::{paths::SanitizedPath, ResultExt};
|
||||||
use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
|
use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
|
||||||
|
|
||||||
use crate::{search::SearchQuery, ProjectPath};
|
use crate::{search::SearchQuery, LspStore, ProjectPath};
|
||||||
|
|
||||||
struct MatchingEntry {
|
struct MatchingEntry {
|
||||||
worktree_path: Arc<Path>,
|
worktree_path: Arc<Path>,
|
||||||
|
@ -69,7 +69,6 @@ impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
||||||
impl WorktreeStore {
|
impl WorktreeStore {
|
||||||
pub fn init(client: &AnyProtoClient) {
|
pub fn init(client: &AnyProtoClient) {
|
||||||
client.add_model_request_handler(Self::handle_create_project_entry);
|
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_copy_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_expand_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))
|
.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<Worktree>, &'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<Entry> {
|
pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
|
||||||
self.worktree_for_id(path.worktree_id, cx)?
|
self.worktree_for_id(path.worktree_id, cx)?
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -1004,16 +1016,56 @@ impl WorktreeStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_rename_project_entry(
|
pub async fn handle_rename_project_entry(
|
||||||
this: Model<Self>,
|
this: Model<super::Project>,
|
||||||
envelope: TypedEnvelope<proto::RenameProjectEntry>,
|
envelope: TypedEnvelope<proto::RenameProjectEntry>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<proto::ProjectEntryResponse> {
|
) -> Result<proto::ProjectEntryResponse> {
|
||||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||||
let worktree = this.update(&mut cx, |this, cx| {
|
let (worktree_id, worktree, old_path, is_dir) = this
|
||||||
this.worktree_for_entry(entry_id, cx)
|
.update(&mut cx, |this, cx| {
|
||||||
.ok_or_else(|| anyhow!("worktree not found"))
|
this.worktree_store
|
||||||
})??;
|
.read(cx)
|
||||||
Worktree::handle_rename_entry(worktree, envelope.payload, cx).await
|
.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(
|
pub async fn handle_copy_project_entry(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue