Detailed changes
@@ -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<Box<dyn PathOpene
ConfirmCodeAction(None),
Some("Editor && showing_code_actions"),
),
+ Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
Binding::new("tab", Tab, Some("Editor")),
Binding::new(
"tab",
@@ -243,6 +247,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("f8", ShowNextDiagnostic, Some("Editor")),
+ Binding::new("f2", Rename, Some("Editor")),
Binding::new("f12", GoToDefinition, Some("Editor")),
Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
Binding::new("pageup", PageUp, Some("Editor")),
@@ -319,6 +324,8 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::toggle_code_actions);
cx.add_async_action(Editor::confirm_completion);
cx.add_async_action(Editor::confirm_code_action);
+ cx.add_async_action(Editor::rename);
+ cx.add_async_action(Editor::confirm_rename);
}
trait SelectionExt {
@@ -2166,79 +2173,88 @@ impl Editor {
let action = actions_menu.actions.get(action_ix)?.clone();
let title = action.lsp_action.title.clone();
let buffer = actions_menu.buffer;
- let replica_id = editor.read(cx).replica_id(cx);
let apply_code_actions = workspace.project().clone().update(cx, |project, cx| {
project.apply_code_action(buffer, action, true, cx)
});
- Some(cx.spawn(|workspace, mut cx| async move {
+ Some(cx.spawn(|workspace, cx| async move {
let project_transaction = apply_code_actions.await?;
+ Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await
+ }))
+ }
- // If the code action's edits are all contained within this editor, then
- // avoid opening a new editor to display them.
- let mut entries = project_transaction.0.iter();
- if let Some((buffer, transaction)) = entries.next() {
- if entries.next().is_none() {
- let excerpt = editor.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(());
- }
+ async fn open_project_transaction(
+ this: ViewHandle<Editor>,
+ workspace: ViewHandle<Workspace>,
+ 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::<usize>(transaction)
- .collect(),
- 1,
- cx,
- ),
- );
- }
- multibuffer.push_transaction(&project_transaction.0);
- multibuffer
- });
+ 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::<usize>(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::<Self>(cx) {
- editor.update(cx, |editor, cx| {
- let settings = (editor.build_settings)(cx);
- editor.highlight_ranges::<Self>(
- ranges_to_highlight,
- settings.style.highlighted_line_background,
- cx,
- );
- });
- }
- });
+ workspace.update(&mut cx, |workspace, cx| {
+ let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx);
+ if let Some(editor) = editor.act_as::<Self>(cx) {
+ editor.update(cx, |editor, cx| {
+ let settings = (editor.build_settings)(cx);
+ editor.highlight_ranges::<Self>(
+ ranges_to_highlight,
+ settings.style.highlighted_line_background,
+ cx,
+ );
+ });
+ }
+ });
- Ok(())
- }))
+ Ok(())
}
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -4072,6 +4088,105 @@ impl Editor {
.detach_and_log_err(cx);
}
+ fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+ 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::<Rename>(vec![highlight_range], Color::red(), cx);
+ });
+ }
+
+ Ok(())
+ }))
+ }
+
+ fn confirm_rename(
+ workspace: &mut Workspace,
+ _: &ConfirmRename,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let editor = workspace.active_item(cx)?.act_as::<Editor>(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::<String>();
+ 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<Anchor>> {
+ self.highlighted_ranges_for_type::<Rename>()
+ .and_then(|(_, range)| range.last())
+ }
+
+ fn take_rename_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<Anchor>> {
+ self.clear_highlighted_ranges::<Rename>(cx)
+ .and_then(|(_, mut ranges)| ranges.pop())
+ }
+
+ fn invalidate_rename_range(
+ &mut self,
+ buffer: &MultiBufferSnapshot,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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::<Rename>(cx);
+ }
+ }
+
fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
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<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
- self.highlighted_ranges.remove(&TypeId::of::<T>());
+ pub fn clear_highlighted_ranges<T: 'static>(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<(Color, Vec<Range<Anchor>>)> {
cx.notify();
+ self.highlighted_ranges.remove(&TypeId::of::<T>())
}
#[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());
@@ -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,
+ ) -> <Self::LspRequest as lsp::request::Request>::Params;
+ fn to_proto(&self, project_id: u64, cx: &AppContext) -> Self::ProtoRequest;
+ fn response_from_lsp(
+ self,
+ message: <Self::LspRequest as lsp::request::Request>::Result,
+ project: ModelHandle<Project>,
+ cx: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<Self::Response>>;
+ fn response_from_proto(
+ self,
+ message: <Self::ProtoRequest as proto::RequestMessage>::Response,
+ project: ModelHandle<Project>,
+ cx: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<Self::Response>>;
+}
+
+pub(crate) struct PrepareRename {
+ pub buffer: ModelHandle<Buffer>,
+ pub position: PointUtf16,
+}
+
+#[derive(Debug)]
+pub(crate) struct PerformRename {
+ pub buffer: ModelHandle<Buffer>,
+ pub position: PointUtf16,
+ pub new_name: String,
+}
+
+impl LspCommand for PrepareRename {
+ type Response = Option<Range<Anchor>>;
+ 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<lsp::PrepareRenameResponse>,
+ _: ModelHandle<Project>,
+ cx: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<Option<Range<Anchor>>>> {
+ 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<Project>,
+ _: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<Option<Range<Anchor>>>> {
+ 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<lsp::WorkspaceEdit>,
+ project: ModelHandle<Project>,
+ mut cx: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<ProjectTransaction>> {
+ 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<Project>,
+ mut cx: AsyncAppContext,
+ ) -> LocalBoxFuture<'static, Result<ProjectTransaction>> {
+ 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()
+ }
+}
@@ -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(),
- })
- }));
- }
- }
-
- 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);
- }
- }
- }
+ Self::deserialize_workspace_edit(
+ this,
+ edit,
+ push_to_history,
+ lang_name,
+ lang_server,
+ &mut cx,
+ )
+ .await
+ } else {
+ Ok(ProjectTransaction::default())
}
-
- 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<Self>,
+ edit: lsp::WorkspaceEdit,
+ push_to_history: bool,
+ language_name: String,
+ language_server: Arc<LanguageServer>,
+ cx: &mut AsyncAppContext,
+ ) -> Result<ProjectTransaction> {
+ 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<T: ToPointUtf16>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: T,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Option<Range<Anchor>>>> {
+ let position = position.to_point_utf16(buffer.read(cx));
+ self.request_lsp(buffer.clone(), PrepareRename { buffer, position }, cx)
+ }
+
+ pub fn perform_rename<T: ToPointUtf16>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ position: T,
+ new_name: String,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<ProjectTransaction>> {
+ let position = position.to_point_utf16(buffer.read(cx));
+ self.request_lsp(
+ buffer.clone(),
+ PerformRename {
+ buffer,
+ position,
+ new_name,
+ },
+ cx,
+ )
+ }
+
+ fn request_lsp<R: LspCommand>(
+ &self,
+ buffer_handle: ModelHandle<Buffer>,
+ request: R,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<R::Response>>
+ where
+ <R::LspRequest as lsp::request::Request>::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::<R::LspRequest>(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<Path>,
@@ -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::<lsp::request::PrepareRenameRequest, _>(|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);
+ }
}
@@ -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;
@@ -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,