Start work on renames

Max Brunsfeld created

Change summary

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(-)

Detailed changes

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<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());

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,
+    ) -> <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()
+    }
+}

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(),
-                            })
-                        }));
-                    }
-                }
-
-                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);
+    }
 }

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;

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,