From 54d76427124d2e79e824ba46ef8a3cb56fac49e4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Feb 2022 12:44:14 -0800 Subject: [PATCH] Start work on renames --- crates/editor/src/editor.rs | 252 ++++++++++++++----- crates/project/src/lsp_command.rs | 192 +++++++++++++++ crates/project/src/project.rs | 389 +++++++++++++++++++++--------- crates/rpc/proto/zed.proto | 28 +++ crates/rpc/src/proto.rs | 7 + 5 files changed, 683 insertions(+), 185 deletions(-) create mode 100644 crates/project/src/lsp_command.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 037b1fea33..b87e1d697b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,8 +24,9 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Binding, platform::CursorStyle, - text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle, + text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; @@ -40,7 +41,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use postage::watch; -use project::Project; +use project::{Project, ProjectTransaction}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; @@ -117,6 +118,8 @@ action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); action!(ShowNextDiagnostic); action!(GoToDefinition); +action!(Rename); +action!(ConfirmRename); action!(PageUp); action!(PageDown); action!(Fold); @@ -153,6 +156,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec= range.end - }) - { - return Ok(()); - } + async fn open_project_transaction( + this: ViewHandle, + workspace: ViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); + + // If the code action's edits are all contained within this editor, then + // avoid opening a new editor to display them. + let mut entries = transaction.0.iter(); + if let Some((buffer, transaction)) = entries.next() { + if entries.next().is_none() { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.newest_anchor_selection().head(), cx) + }); + if let Some((excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + let excerpt_range = excerpt_range.to_offset(&snapshot); + if snapshot + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + { + return Ok(()); } } } } + } - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &project_transaction.0 { - let snapshot = buffer.read(cx).snapshot(); - ranges_to_highlight.extend( - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot - .edited_ranges_for_transaction::(transaction) - .collect(), - 1, - cx, - ), + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer, transaction) in &transaction.0 { + let snapshot = buffer.read(cx).snapshot(); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + snapshot + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(&transaction.0); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let settings = (editor.build_settings)(cx); + editor.highlight_ranges::( + ranges_to_highlight, + settings.style.highlighted_line_background, + cx, ); - } - multibuffer.push_transaction(&project_transaction.0); - multibuffer - }); + }); + } + }); - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let settings = (editor.build_settings)(cx); - editor.highlight_ranges::( - ranges_to_highlight, - settings.style.highlighted_line_background, - cx, - ); - }); - } - }); - - Ok(()) - })) + Ok(()) } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { @@ -4072,6 +4088,105 @@ impl Editor { .detach_and_log_err(cx); } + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let position = self.newest_anchor_selection().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx)?; + let snapshot = buffer.read(cx).snapshot(); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(buffer.clone(), buffer_position.to_offset(&snapshot), cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + if let Some(range) = prepare_rename.await? { + let buffer_offset_range = range.to_offset(&snapshot); + let buffer_offset = buffer_position.to_offset(&snapshot); + let lookbehind = buffer_offset.saturating_sub(buffer_offset_range.start); + let lookahead = buffer_offset_range.end.saturating_sub(buffer_offset); + + this.update(&mut cx, |this, cx| { + let buffer = this.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let start = offset - lookbehind; + let end = offset + lookahead; + let highlight_range = buffer.anchor_before(start)..buffer.anchor_after(end); + drop(buffer); + + this.select_ranges([start..end], None, cx); + this.highlight_ranges::(vec![highlight_range], Color::red(), cx); + }); + } + + Ok(()) + })) + } + + fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, position, new_name) = editor.update(cx, |editor, cx| { + let range = editor.take_rename_range(cx)?; + let multibuffer = editor.buffer.read(cx); + let (buffer, position) = + multibuffer.text_anchor_for_position(range.start.clone(), cx)?; + let snapshot = multibuffer.read(cx); + let new_name = snapshot.text_for_range(range.clone()).collect::(); + Some((buffer, position, new_name)) + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename(buffer, position, new_name.clone(), cx) + }); + + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + editor, + workspace, + project_transaction, + format!("Rename: {}", new_name), + cx, + ) + .await + })) + } + + fn rename_range(&self) -> Option<&Range> { + self.highlighted_ranges_for_type::() + .and_then(|(_, range)| range.last()) + } + + fn take_rename_range(&mut self, cx: &mut ViewContext) -> Option> { + self.clear_highlighted_ranges::(cx) + .and_then(|(_, mut ranges)| ranges.pop()) + } + + fn invalidate_rename_range( + &mut self, + buffer: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) { + if let Some(range) = &self.rename_range() { + if self.selections.len() == 1 { + let head = self.selections[0].head().to_offset(&buffer); + if range.start.to_offset(&buffer) <= head && range.end.to_offset(&buffer) >= head { + return; + } + } + eprintln!("clearing highlight range"); + self.clear_highlighted_ranges::(cx); + } + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); @@ -4484,6 +4599,7 @@ impl Editor { self.select_larger_syntax_node_stack.clear(); self.autoclose_stack.invalidate(&self.selections, &buffer); self.snippet_stack.invalidate(&self.selections, &buffer); + self.invalidate_rename_range(&buffer, cx); let new_cursor_position = self.newest_anchor_selection().head(); @@ -4759,9 +4875,12 @@ impl Editor { cx.notify(); } - pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { - self.highlighted_ranges.remove(&TypeId::of::()); + pub fn clear_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Color, Vec>)> { cx.notify(); + self.highlighted_ranges.remove(&TypeId::of::()) } #[cfg(feature = "test-support")] @@ -5091,6 +5210,9 @@ impl View for Editor { EditorMode::Full => "full", }; cx.map.insert("mode".into(), mode.into()); + if self.rename_range().is_some() { + cx.set.insert("renaming".into()); + } match self.context_menu.as_ref() { Some(ContextMenu::Completions(_)) => { cx.set.insert("showing_completions".into()); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs new file mode 100644 index 0000000000..9e2f8643b3 --- /dev/null +++ b/crates/project/src/lsp_command.rs @@ -0,0 +1,192 @@ +use crate::{Project, ProjectTransaction}; +use anyhow::{anyhow, Result}; +use client::proto; +use futures::{future::LocalBoxFuture, FutureExt}; +use gpui::{AppContext, AsyncAppContext, ModelHandle}; +use language::{ + proto::deserialize_anchor, range_from_lsp, Anchor, Buffer, PointUtf16, ToLspPosition, +}; +use std::{ops::Range, path::Path}; + +pub(crate) trait LspCommand: 'static { + type Response: 'static + Default + Send; + type LspRequest: 'static + Send + lsp::request::Request; + type ProtoRequest: 'static + Send + proto::RequestMessage; + + fn to_lsp( + &self, + path: &Path, + cx: &AppContext, + ) -> ::Params; + fn to_proto(&self, project_id: u64, cx: &AppContext) -> Self::ProtoRequest; + fn response_from_lsp( + self, + message: ::Result, + project: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>; + fn response_from_proto( + self, + message: ::Response, + project: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>; +} + +pub(crate) struct PrepareRename { + pub buffer: ModelHandle, + pub position: PointUtf16, +} + +#[derive(Debug)] +pub(crate) struct PerformRename { + pub buffer: ModelHandle, + pub position: PointUtf16, + pub new_name: String, +} + +impl LspCommand for PrepareRename { + type Response = Option>; + type LspRequest = lsp::request::PrepareRenameRequest; + type ProtoRequest = proto::PrepareRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::TextDocumentPositionParams { + lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + } + } + + fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PrepareRename { + let buffer_id = self.buffer.read(cx).remote_id(); + proto::PrepareRename { + project_id, + buffer_id, + position: None, + } + } + + fn response_from_lsp( + self, + message: Option, + _: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>>> { + async move { + Ok(message.and_then(|result| match result { + lsp::PrepareRenameResponse::Range(range) + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => { + self.buffer.read_with(&cx, |buffer, _| { + let range = range_from_lsp(range); + Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)) + }) + } + _ => None, + })) + } + .boxed_local() + } + + fn response_from_proto( + self, + message: proto::PrepareRenameResponse, + _: ModelHandle, + _: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>>> { + async move { + if message.can_rename { + let start = message.start.and_then(deserialize_anchor); + let end = message.end.and_then(deserialize_anchor); + Ok(start.zip(end).map(|(start, end)| start..end)) + } else { + Ok(None) + } + } + .boxed_local() + } +} + +impl LspCommand for PerformRename { + type Response = ProjectTransaction; + type LspRequest = lsp::request::Rename; + type ProtoRequest = proto::PerformRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::RenameParams { + lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + }, + new_name: self.new_name.clone(), + work_done_progress_params: Default::default(), + } + } + + fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PerformRename { + let buffer_id = self.buffer.read(cx).remote_id(); + proto::PerformRename { + project_id, + buffer_id, + position: None, + new_name: self.new_name.clone(), + } + } + + fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + mut cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result> { + async move { + if let Some(edit) = message { + let (language_name, language_server) = + self.buffer.read_with(&cx, |buffer, _| { + let language = buffer + .language() + .ok_or_else(|| anyhow!("buffer's language was removed"))?; + let language_server = buffer + .language_server() + .cloned() + .ok_or_else(|| anyhow!("buffer's language server was removed"))?; + Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) + })?; + Project::deserialize_workspace_edit( + project, + edit, + false, + language_name, + language_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) + } + } + .boxed_local() + } + + fn response_from_proto( + self, + message: proto::PerformRenameResponse, + project: ModelHandle, + mut cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result> { + async move { + let message = message + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project + .update(&mut cx, |project, cx| { + project.deserialize_project_transaction(message, false, cx) + }) + .await + } + .boxed_local() + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6dc7c2c231..71ec078f4e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ pub mod fs; mod ignore; +mod lsp_command; pub mod worktree; use anyhow::{anyhow, Context, Result}; @@ -15,11 +16,12 @@ use gpui::{ use language::{ point_from_lsp, proto::{deserialize_anchor, serialize_anchor}, - range_from_lsp, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, + range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, LanguageServer}; +use lsp_command::*; use postage::{broadcast, prelude::Stream, sink::Sink, watch}; use smol::block_on; use std::{ @@ -1625,7 +1627,6 @@ impl Project { return Task::ready(Err(anyhow!("buffer does not have a language server"))); }; let range = action.range.to_point_utf16(buffer); - let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { if let Some(lsp_range) = action @@ -1656,126 +1657,19 @@ impl Project { .lsp_action; } - let mut operations = Vec::new(); if let Some(edit) = action.lsp_action.edit { - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => operations - .extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)), - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(lsp::OneOf::Left).collect(), - }) - })); - } + Self::deserialize_workspace_edit( + this, + edit, + push_to_history, + lang_name, + lang_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) } - - let mut project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - fs.rename( - &source_abs_path, - &target_abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op.options.map(Into::into).unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(&mut cx, |this, cx| { - this.open_local_buffer_from_lsp_path( - op.text_document.uri, - lang_name.clone(), - lang_server.clone(), - cx, - ) - }) - .await?; - - let edits = buffer_to_edit - .update(&mut cx, |buffer, cx| { - let edits = op.edits.into_iter().map(|edit| match edit { - lsp::OneOf::Left(edit) => edit, - lsp::OneOf::Right(edit) => edit.text_edit, - }); - buffer.edits_from_lsp(edits, op.text_document.version, cx) - }) - .await?; - - let transaction = buffer_to_edit.update(&mut cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([range], text, cx); - } - let transaction = if buffer.end_transaction(cx).is_some() { - let transaction = - buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - - transaction - }); - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -1800,6 +1694,194 @@ impl Project { } } + async fn deserialize_workspace_edit( + this: ModelHandle, + edit: lsp::WorkspaceEdit, + push_to_history: bool, + language_name: String, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let fs = this.read_with(cx, |this, _| this.fs.clone()); + let mut operations = Vec::new(); + if let Some(document_changes) = edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(edits) => { + operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) + } + lsp::DocumentChanges::Operations(ops) => operations = ops, + } + } else if let Some(changes) = edit.changes { + operations.extend(changes.into_iter().map(|(uri, edits)| { + lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { + text_document: lsp::OptionalVersionedTextDocumentIdentifier { + uri, + version: None, + }, + edits: edits.into_iter().map(lsp::OneOf::Left).collect(), + }) + })); + } + + let mut project_transaction = ProjectTransaction::default(); + for operation in operations { + match operation { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = abs_path.parent() { + fs.create_dir(parent_path).await?; + } + if abs_path.ends_with("/") { + fs.create_dir(&abs_path).await?; + } else { + fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) + .await?; + } + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { + let source_abs_path = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target_abs_path = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source_abs_path, + &target_abs_path, + op.options.map(Into::into).unwrap_or_default(), + ) + .await?; + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op.options.map(Into::into).unwrap_or_default(); + if abs_path.ends_with("/") { + fs.remove_dir(&abs_path, options).await?; + } else { + fs.remove_file(&abs_path, options).await?; + } + } + lsp::DocumentChangeOperation::Edit(op) => { + let buffer_to_edit = this + .update(cx, |this, cx| { + this.open_local_buffer_from_lsp_path( + op.text_document.uri, + language_name.clone(), + language_server.clone(), + cx, + ) + }) + .await?; + + let edits = buffer_to_edit + .update(cx, |buffer, cx| { + let edits = op.edits.into_iter().map(|edit| match edit { + lsp::OneOf::Left(edit) => edit, + lsp::OneOf::Right(edit) => edit.text_edit, + }); + buffer.edits_from_lsp(edits, op.text_document.version, cx) + }) + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([range], text, cx); + } + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + + transaction + }); + if let Some(transaction) = transaction { + project_transaction.0.insert(buffer_to_edit, transaction); + } + } + } + } + + Ok(project_transaction) + } + + pub fn prepare_rename( + &self, + buffer: ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), PrepareRename { buffer, position }, cx) + } + + pub fn perform_rename( + &self, + buffer: ModelHandle, + position: T, + new_name: String, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + PerformRename { + buffer, + position, + new_name, + }, + cx, + ) + } + + fn request_lsp( + &self, + buffer_handle: ModelHandle, + request: R, + cx: &mut ModelContext, + ) -> Task> + where + ::Result: Send, + { + let buffer = buffer_handle.read(cx); + if self.is_local() { + let file = File::from_dyn(buffer.file()).and_then(File::as_local); + if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { + let lsp_params = request.to_lsp(&file.abs_path(cx), cx); + return cx.spawn(|this, cx| async move { + let response = language_server + .request::(lsp_params) + .await + .context("lsp request failed")?; + request.response_from_lsp(response, this, cx).await + }); + } + } else if let Some(project_id) = self.remote_id() { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, cx); + return cx.spawn(|this, cx| async move { + let response = rpc.request(message).await?; + request.response_from_proto(response, this, cx).await + }); + } + Task::ready(Ok(Default::default())) + } + pub fn find_or_create_local_worktree( &self, abs_path: impl AsRef, @@ -4099,4 +4181,71 @@ mod tests { ] ); } + + #[gpui::test] + async fn test_rename(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + + let project = Project::test(fs.clone(), &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); + }); + + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer((worktree_id, Path::new("one.rs")), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + + let response = project.update(&mut cx, |project, cx| { + project.prepare_rename(buffer.clone(), 7, cx) + }); + fake_server + .handle_request::(|params| { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ))) + }) + .next() + .await + .unwrap(); + let range = response.await.unwrap().unwrap(); + let range = buffer.read_with(&cx, |buffer, _| range.to_offset(buffer)); + assert_eq!(range, 6..9); + } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9d7baa8992..65622e70d4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -50,6 +50,10 @@ message Envelope { GetCodeActionsResponse get_code_actions_response = 42; ApplyCodeAction apply_code_action = 43; ApplyCodeActionResponse apply_code_action_response = 44; + PrepareRename prepare_rename = 58; + PrepareRenameResponse prepare_rename_response = 59; + PerformRename perform_rename = 60; + PerformRenameResponse perform_rename_response = 61; GetChannels get_channels = 45; GetChannelsResponse get_channels_response = 46; @@ -274,6 +278,30 @@ message ApplyCodeActionResponse { ProjectTransaction transaction = 1; } +message PrepareRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message PrepareRenameResponse { + bool can_rename = 1; + Anchor start = 2; + Anchor end = 3; + repeated VectorClockEntry version = 4; +} + +message PerformRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string new_name = 4; +} + +message PerformRenameResponse { + ProjectTransaction transaction = 2; +} + message CodeAction { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8093f2551f..b5f1d49c06 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -167,6 +167,10 @@ messages!( (LeaveProject, Foreground), (OpenBuffer, Foreground), (OpenBufferResponse, Foreground), + (PerformRename, Background), + (PerformRenameResponse, Background), + (PrepareRename, Background), + (PrepareRenameResponse, Background), (RegisterProjectResponse, Foreground), (Ping, Foreground), (RegisterProject, Foreground), @@ -205,6 +209,8 @@ request_messages!( (JoinProject, JoinProjectResponse), (OpenBuffer, OpenBufferResponse), (Ping, Ack), + (PerformRename, PerformRenameResponse), + (PrepareRename, PrepareRenameResponse), (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (SaveBuffer, BufferSaved), @@ -233,6 +239,7 @@ entity_messages!( JoinProject, LeaveProject, OpenBuffer, + PrepareRename, RemoveProjectCollaborator, SaveBuffer, ShareWorktree,