Merge pull request #455 from zed-industries/rename

Antonio Scandurra created

Introduce rename support via `F2`

Change summary

crates/client/src/client.rs         |  27 
crates/editor/src/editor.rs         | 395 +++++++++++++--
crates/editor/src/element.rs        |  24 
crates/editor/src/multi_buffer.rs   |   1 
crates/project/src/fs.rs            |  22 
crates/project/src/lsp_command.rs   | 449 +++++++++++++++++
crates/project/src/project.rs       | 780 ++++++++++++++++--------------
crates/project/src/worktree.rs      |   2 
crates/rpc/proto/zed.proto          |  28 +
crates/rpc/src/proto.rs             |   8 
crates/server/src/rpc.rs            | 314 +++++++++++
crates/text/src/text.rs             |   2 
crates/theme/src/theme.rs           |   4 
crates/workspace/src/workspace.rs   |   2 
crates/zed/assets/themes/_base.toml |   4 
crates/zed/assets/themes/black.toml |   4 
crates/zed/assets/themes/dark.toml  |   4 
crates/zed/assets/themes/light.toml |   4 
crates/zed/src/test.rs              |   2 
crates/zed/src/zed.rs               |  18 
20 files changed, 1,604 insertions(+), 490 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -817,19 +817,28 @@ impl Client {
         self.peer.send(self.connection_id()?, message)
     }
 
-    pub async fn request<T: RequestMessage>(&self, request: T) -> Result<T::Response> {
+    pub fn request<T: RequestMessage>(
+        &self,
+        request: T,
+    ) -> impl Future<Output = Result<T::Response>> {
+        let client_id = self.id;
         log::debug!(
             "rpc request start. client_id: {}. name:{}",
-            self.id,
-            T::NAME
-        );
-        let response = self.peer.request(self.connection_id()?, request).await;
-        log::debug!(
-            "rpc request finish. client_id: {}. name:{}",
-            self.id,
+            client_id,
             T::NAME
         );
-        response
+        let response = self
+            .connection_id()
+            .map(|conn_id| self.peer.request(conn_id, request));
+        async move {
+            let response = response?.await;
+            log::debug!(
+                "rpc request finish. client_id: {}. name:{}",
+                client_id,
+                T::NAME
+            );
+            response
+        }
     }
 
     fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {

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 {
@@ -432,6 +439,7 @@ pub struct Editor {
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
     code_actions_task: Option<Task<()>>,
+    pending_rename: Option<RenameState>,
 }
 
 pub struct EditorSnapshot {
@@ -470,6 +478,13 @@ struct SnippetState {
     active_index: usize,
 }
 
+pub struct RenameState {
+    pub range: Range<Anchor>,
+    pub old_name: String,
+    pub editor: ViewHandle<Editor>,
+    block_id: BlockId,
+}
+
 struct InvalidationStack<T>(Vec<T>);
 
 enum ContextMenu {
@@ -885,6 +900,7 @@ impl Editor {
             next_completion_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
+            pending_rename: Default::default(),
         };
         this.end_selection(cx);
         this
@@ -1438,6 +1454,10 @@ impl Editor {
     }
 
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if self.take_rename(cx).is_some() {
+            return;
+        }
+
         if self.hide_context_menu(cx).is_some() {
             return;
         }
@@ -1906,6 +1926,10 @@ impl Editor {
     }
 
     fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
+        if self.pending_rename.is_some() {
+            return;
+        }
+
         let project = if let Some(project) = self.project.clone() {
             project
         } else {
@@ -2153,79 +2177,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<()> {
@@ -3130,6 +3163,10 @@ impl Editor {
     }
 
     pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
+        if self.take_rename(cx).is_some() {
+            return;
+        }
+
         if let Some(context_menu) = self.context_menu.as_mut() {
             if context_menu.select_prev(cx) {
                 return;
@@ -3174,6 +3211,8 @@ impl Editor {
     }
 
     pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
+        self.take_rename(cx);
+
         if let Some(context_menu) = self.context_menu.as_mut() {
             if context_menu.select_next(cx) {
                 return;
@@ -4059,6 +4098,219 @@ impl Editor {
         .detach_and_log_err(cx);
     }
 
+    pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
+
+        let project = self.project.clone()?;
+        let selection = self.newest_anchor_selection().clone();
+        let (cursor_buffer, cursor_buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.head(), cx)?;
+        let (tail_buffer, tail_buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.tail(), cx)?;
+        if tail_buffer != cursor_buffer {
+            return None;
+        }
+
+        let snapshot = cursor_buffer.read(cx).snapshot();
+        let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
+        let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot);
+        let prepare_rename = project.update(cx, |project, cx| {
+            project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            if let Some(rename_range) = prepare_rename.await? {
+                let rename_buffer_range = rename_range.to_offset(&snapshot);
+                let cursor_offset_in_rename_range =
+                    cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
+                let tail_offset_in_rename_range =
+                    tail_buffer_offset.saturating_sub(rename_buffer_range.start);
+
+                this.update(&mut cx, |this, cx| {
+                    this.take_rename(cx);
+                    let settings = (this.build_settings)(cx);
+                    let buffer = this.buffer.read(cx).read(cx);
+                    let cursor_offset = selection.head().to_offset(&buffer);
+                    let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
+                    let rename_end = rename_start + rename_buffer_range.len();
+                    let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+                    let old_name = buffer
+                        .text_for_range(rename_start..rename_end)
+                        .collect::<String>();
+                    drop(buffer);
+
+                    // Position the selection in the rename editor so that it matches the current selection.
+                    let rename_editor = cx.add_view(|cx| {
+                        let mut editor = Editor::single_line(this.build_settings.clone(), cx);
+                        editor
+                            .buffer
+                            .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx));
+                        editor.select_ranges(
+                            [tail_offset_in_rename_range..cursor_offset_in_rename_range],
+                            None,
+                            cx,
+                        );
+                        editor.highlight_ranges::<Rename>(
+                            vec![Anchor::min()..Anchor::max()],
+                            settings.style.diff_background_inserted,
+                            cx,
+                        );
+                        editor
+                    });
+                    this.highlight_ranges::<Rename>(
+                        vec![range.clone()],
+                        settings.style.diff_background_deleted,
+                        cx,
+                    );
+                    this.update_selections(
+                        vec![Selection {
+                            id: selection.id,
+                            start: rename_end,
+                            end: rename_end,
+                            reversed: false,
+                            goal: SelectionGoal::None,
+                        }],
+                        None,
+                        cx,
+                    );
+                    cx.focus(&rename_editor);
+                    let block_id = this.insert_blocks(
+                        [BlockProperties {
+                            position: range.start.clone(),
+                            height: 1,
+                            render: Arc::new({
+                                let editor = rename_editor.clone();
+                                move |cx: &BlockContext| {
+                                    ChildView::new(editor.clone())
+                                        .contained()
+                                        .with_padding_left(cx.anchor_x)
+                                        .boxed()
+                                }
+                            }),
+                            disposition: BlockDisposition::Below,
+                        }],
+                        cx,
+                    )[0];
+                    this.pending_rename = Some(RenameState {
+                        range,
+                        old_name,
+                        editor: rename_editor,
+                        block_id,
+                    });
+                });
+            }
+
+            Ok(())
+        }))
+    }
+
+    pub 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, range, old_name, new_name) = editor.update(cx, |editor, cx| {
+            let rename = editor.take_rename(cx)?;
+            let buffer = editor.buffer.read(cx);
+            let (start_buffer, start) =
+                buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
+            let (end_buffer, end) =
+                buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
+            if start_buffer == end_buffer {
+                let new_name = rename.editor.read(cx).text(cx);
+                Some((start_buffer, start..end, rename.old_name, new_name))
+            } else {
+                None
+            }
+        })?;
+
+        let rename = workspace.project().clone().update(cx, |project, cx| {
+            project.perform_rename(
+                buffer.clone(),
+                range.start.clone(),
+                new_name.clone(),
+                true,
+                cx,
+            )
+        });
+
+        Some(cx.spawn(|workspace, cx| async move {
+            let project_transaction = rename.await?;
+            Self::open_project_transaction(
+                editor,
+                workspace,
+                project_transaction,
+                format!("Rename: {} → {}", old_name, new_name),
+                cx,
+            )
+            .await
+        }))
+    }
+
+    fn take_rename(&mut self, cx: &mut ViewContext<Self>) -> Option<RenameState> {
+        let rename = self.pending_rename.take()?;
+        self.remove_blocks([rename.block_id].into_iter().collect(), cx);
+        self.clear_highlighted_ranges::<Rename>(cx);
+
+        let editor = rename.editor.read(cx);
+        let buffer = editor.buffer.read(cx).snapshot(cx);
+        let selection = editor.newest_selection::<usize>(&buffer);
+
+        // Update the selection to match the position of the selection inside
+        // the rename editor.
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let rename_range = rename.range.to_offset(&snapshot);
+        let start = snapshot
+            .clip_offset(rename_range.start + selection.start, Bias::Left)
+            .min(rename_range.end);
+        let end = snapshot
+            .clip_offset(rename_range.start + selection.end, Bias::Left)
+            .min(rename_range.end);
+        self.update_selections(
+            vec![Selection {
+                id: self.newest_anchor_selection().id,
+                start,
+                end,
+                reversed: selection.reversed,
+                goal: SelectionGoal::None,
+            }],
+            None,
+            cx,
+        );
+
+        Some(rename)
+    }
+
+    fn invalidate_rename_range(
+        &mut self,
+        buffer: &MultiBufferSnapshot,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(rename) = self.pending_rename.as_ref() {
+            if self.selections.len() == 1 {
+                let head = self.selections[0].head().to_offset(buffer);
+                let range = rename.range.to_offset(buffer).to_inclusive();
+                if range.contains(&head) {
+                    return;
+                }
+            }
+            let rename = self.pending_rename.take().unwrap();
+            self.remove_blocks([rename.block_id].into_iter().collect(), cx);
+            self.clear_highlighted_ranges::<Rename>(cx);
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn pending_rename(&self) -> Option<&RenameState> {
+        self.pending_rename.as_ref()
+    }
+
     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);
@@ -4471,6 +4723,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();
 
@@ -4746,9 +4999,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")]
@@ -4958,6 +5214,8 @@ impl EditorSettings {
                     gutter_padding_factor: 2.,
                     active_line_background: Default::default(),
                     highlighted_line_background: Default::default(),
+                    diff_background_deleted: Default::default(),
+                    diff_background_inserted: Default::default(),
                     line_number: Default::default(),
                     line_number_active: Default::default(),
                     selection: Default::default(),
@@ -5078,6 +5336,9 @@ impl View for Editor {
             EditorMode::Full => "full",
         };
         cx.map.insert("mode".into(), mode.into());
+        if self.pending_rename.is_some() {
+            cx.set.insert("renaming".into());
+        }
         match self.context_menu.as_ref() {
             Some(ContextMenu::Completions(_)) => {
                 cx.set.insert("showing_completions".into());
@@ -7747,8 +8008,8 @@ mod tests {
         "
         .unindent();
 
-        let fs = Arc::new(FakeFs::new(cx.background().clone()));
-        fs.insert_file("/file", text).await.unwrap();
+        let fs = FakeFs::new(cx.background().clone());
+        fs.insert_file("/file", text).await;
 
         let project = Project::test(fs, &mut cx);
 

crates/editor/src/element.rs 🔗

@@ -299,7 +299,7 @@ impl EditorElement {
         if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
             let mut x = bounds.width() - layout.gutter_padding;
             let mut y = *row as f32 * layout.line_height - scroll_top;
-            x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.;
+            x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
             y += (layout.line_height - indicator.size().y()) / 2.;
             indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
         }
@@ -321,7 +321,7 @@ impl EditorElement {
         let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
         let max_glyph_width = layout.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
-        let content_origin = bounds.origin() + layout.text_offset;
+        let content_origin = bounds.origin() + layout.gutter_margin;
 
         cx.scene.push_layer(Some(bounds));
 
@@ -776,22 +776,24 @@ impl Element for EditorElement {
 
         let gutter_padding;
         let gutter_width;
+        let gutter_margin;
         if snapshot.mode == EditorMode::Full {
             gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor;
             gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+            gutter_margin = -style.text.descent(cx.font_cache);
         } else {
             gutter_padding = 0.0;
-            gutter_width = 0.0
+            gutter_width = 0.0;
+            gutter_margin = 0.0;
         };
 
         let text_width = size.x() - gutter_width;
-        let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.);
         let em_width = style.text.em_width(cx.font_cache);
         let em_advance = style.text.em_advance(cx.font_cache);
         let overscroll = vec2f(em_width, 0.);
         let wrap_width = match self.settings.soft_wrap {
             SoftWrap::None => None,
-            SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width),
+            SoftWrap::EditorWidth => Some(text_width - gutter_margin - overscroll.x() - em_width),
             SoftWrap::Column(column) => Some(column as f32 * em_advance),
         };
         let snapshot = self.update_view(cx.app, |view, cx| {
@@ -991,7 +993,7 @@ impl Element for EditorElement {
             gutter_padding,
             gutter_width,
             em_width,
-            gutter_width + text_offset.x(),
+            gutter_width + gutter_margin,
             line_height,
             &style,
             &line_layouts,
@@ -1006,7 +1008,7 @@ impl Element for EditorElement {
                 gutter_size,
                 gutter_padding,
                 text_size,
-                text_offset,
+                gutter_margin,
                 snapshot,
                 active_rows,
                 highlighted_rows,
@@ -1080,6 +1082,12 @@ impl Element for EditorElement {
             }
         }
 
+        for (_, block) in &mut layout.blocks {
+            if block.dispatch_event(event, cx) {
+                return true;
+            }
+        }
+
         match event {
             Event::LeftMouseDown {
                 position,
@@ -1123,6 +1131,7 @@ pub struct LayoutState {
     scroll_max: Vector2F,
     gutter_size: Vector2F,
     gutter_padding: f32,
+    gutter_margin: f32,
     text_size: Vector2F,
     snapshot: EditorSnapshot,
     active_rows: BTreeMap<u32, bool>,
@@ -1135,7 +1144,6 @@ pub struct LayoutState {
     em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
-    text_offset: Vector2F,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
 }

crates/editor/src/multi_buffer.rs 🔗

@@ -58,6 +58,7 @@ pub enum CharKind {
     Word,
 }
 
+#[derive(Clone)]
 struct Transaction {
     id: TransactionId,
     buffer_transactions: HashMap<usize, text::TransactionId>,

crates/project/src/fs.rs 🔗

@@ -268,7 +268,7 @@ pub struct FakeFs {
 
 #[cfg(any(test, feature = "test-support"))]
 impl FakeFs {
-    pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> Self {
+    pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
         let (events_tx, _) = postage::broadcast::channel(2048);
         let mut entries = std::collections::BTreeMap::new();
         entries.insert(
@@ -283,20 +283,20 @@ impl FakeFs {
                 content: None,
             },
         );
-        Self {
+        std::sync::Arc::new(Self {
             executor,
             state: futures::lock::Mutex::new(FakeFsState {
                 entries,
                 next_inode: 1,
                 events_tx,
             }),
-        }
+        })
     }
 
-    pub async fn insert_dir(&self, path: impl AsRef<Path>) -> Result<()> {
+    pub async fn insert_dir(&self, path: impl AsRef<Path>) {
         let mut state = self.state.lock().await;
         let path = path.as_ref();
-        state.validate_path(path)?;
+        state.validate_path(path).unwrap();
 
         let inode = state.next_inode;
         state.next_inode += 1;
@@ -313,13 +313,12 @@ impl FakeFs {
             },
         );
         state.emit_event(&[path]).await;
-        Ok(())
     }
 
-    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
         let mut state = self.state.lock().await;
         let path = path.as_ref();
-        state.validate_path(path)?;
+        state.validate_path(path).unwrap();
 
         let inode = state.next_inode;
         state.next_inode += 1;
@@ -336,7 +335,6 @@ impl FakeFs {
             },
         );
         state.emit_event(&[path]).await;
-        Ok(())
     }
 
     #[must_use]
@@ -353,7 +351,7 @@ impl FakeFs {
 
             match tree {
                 Object(map) => {
-                    self.insert_dir(path).await.unwrap();
+                    self.insert_dir(path).await;
                     for (name, contents) in map {
                         let mut path = PathBuf::from(path);
                         path.push(name);
@@ -361,10 +359,10 @@ impl FakeFs {
                     }
                 }
                 Null => {
-                    self.insert_dir(&path).await.unwrap();
+                    self.insert_dir(&path).await;
                 }
                 String(contents) => {
-                    self.insert_file(&path, contents).await.unwrap();
+                    self.insert_file(&path, contents).await;
                 }
                 _ => {
                     panic!("JSON object must contain only objects, strings, or null");

crates/project/src/lsp_command.rs 🔗

@@ -0,0 +1,449 @@
+use crate::{Definition, Project, ProjectTransaction};
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use client::{proto, PeerId};
+use gpui::{AppContext, AsyncAppContext, ModelHandle};
+use language::{
+    point_from_lsp,
+    proto::{deserialize_anchor, serialize_anchor},
+    range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16,
+};
+use std::{ops::Range, path::Path};
+
+#[async_trait(?Send)]
+pub(crate) trait LspCommand: 'static + Sized {
+    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;
+    async fn response_from_lsp(
+        self,
+        message: <Self::LspRequest as lsp::request::Request>::Result,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Self::Response>;
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
+    fn from_proto(
+        message: Self::ProtoRequest,
+        project: &mut Project,
+        buffer: &Buffer,
+    ) -> Result<Self>;
+    fn response_to_proto(
+        response: Self::Response,
+        project: &mut Project,
+        peer_id: PeerId,
+        buffer_version: &clock::Global,
+        cx: &AppContext,
+    ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
+    async fn response_from_proto(
+        self,
+        message: <Self::ProtoRequest as proto::RequestMessage>::Response,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Self::Response>;
+    fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64;
+}
+
+pub(crate) struct PrepareRename {
+    pub position: PointUtf16,
+}
+
+pub(crate) struct PerformRename {
+    pub position: PointUtf16,
+    pub new_name: String,
+    pub push_to_history: bool,
+}
+
+pub(crate) struct GetDefinition {
+    pub position: PointUtf16,
+}
+
+#[async_trait(?Send)]
+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(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::PrepareRenameResponse>,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Option<Range<Anchor>>> {
+        buffer.read_with(&cx, |buffer, _| {
+            if let Some(
+                lsp::PrepareRenameResponse::Range(range)
+                | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. },
+            ) = message
+            {
+                let Range { start, end } = range_from_lsp(range);
+                if buffer.clip_point_utf16(start, Bias::Left) == start
+                    && buffer.clip_point_utf16(end, Bias::Left) == end
+                {
+                    return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end)));
+                }
+            }
+            Ok(None)
+        })
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename {
+        proto::PrepareRename {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+        }
+    }
+
+    fn from_proto(message: proto::PrepareRename, _: &mut Project, buffer: &Buffer) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        if !buffer.can_resolve(&position) {
+            Err(anyhow!("cannot resolve position"))?;
+        }
+        Ok(Self {
+            position: position.to_point_utf16(buffer),
+        })
+    }
+
+    fn response_to_proto(
+        range: Option<Range<Anchor>>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &AppContext,
+    ) -> proto::PrepareRenameResponse {
+        proto::PrepareRenameResponse {
+            can_rename: range.is_some(),
+            start: range
+                .as_ref()
+                .map(|range| language::proto::serialize_anchor(&range.start)),
+            end: range
+                .as_ref()
+                .map(|range| language::proto::serialize_anchor(&range.end)),
+            version: buffer_version.into(),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::PrepareRenameResponse,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Option<Range<Anchor>>> {
+        if message.can_rename {
+            buffer
+                .update(&mut cx, |buffer, _| {
+                    buffer.wait_for_version(message.version.into())
+                })
+                .await;
+            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)
+        }
+    }
+
+    fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait(?Send)]
+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(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::WorkspaceEdit>,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        if let Some(edit) = message {
+            let (language_name, language_server) = 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,
+                self.push_to_history,
+                language_name,
+                language_server,
+                &mut cx,
+            )
+            .await
+        } else {
+            Ok(ProjectTransaction::default())
+        }
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename {
+        proto::PerformRename {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            new_name: self.new_name.clone(),
+        }
+    }
+
+    fn from_proto(message: proto::PerformRename, _: &mut Project, buffer: &Buffer) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        if !buffer.can_resolve(&position) {
+            Err(anyhow!("cannot resolve position"))?;
+        }
+        Ok(Self {
+            position: position.to_point_utf16(buffer),
+            new_name: message.new_name,
+            push_to_history: false,
+        })
+    }
+
+    fn response_to_proto(
+        response: ProjectTransaction,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &AppContext,
+    ) -> proto::PerformRenameResponse {
+        let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx);
+        proto::PerformRenameResponse {
+            transaction: Some(transaction),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::PerformRenameResponse,
+        project: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ProjectTransaction> {
+        let message = message
+            .transaction
+            .ok_or_else(|| anyhow!("missing transaction"))?;
+        project
+            .update(&mut cx, |project, cx| {
+                project.deserialize_project_transaction(message, self.push_to_history, cx)
+            })
+            .await
+    }
+
+    fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 {
+        message.buffer_id
+    }
+}
+
+#[async_trait(?Send)]
+impl LspCommand for GetDefinition {
+    type Response = Vec<Definition>;
+    type LspRequest = lsp::request::GotoDefinition;
+    type ProtoRequest = proto::GetDefinition;
+
+    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoDefinitionParams {
+        lsp::GotoDefinitionParams {
+            text_document_position_params: lsp::TextDocumentPositionParams {
+                text_document: lsp::TextDocumentIdentifier {
+                    uri: lsp::Url::from_file_path(path).unwrap(),
+                },
+                position: self.position.to_lsp_position(),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::GotoDefinitionResponse>,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Definition>> {
+        let mut definitions = Vec::new();
+        let (language, language_server) = buffer
+            .read_with(&cx, |buffer, _| {
+                buffer
+                    .language()
+                    .cloned()
+                    .zip(buffer.language_server().cloned())
+            })
+            .ok_or_else(|| anyhow!("buffer no longer has language server"))?;
+
+        if let Some(message) = message {
+            let mut unresolved_locations = Vec::new();
+            match message {
+                lsp::GotoDefinitionResponse::Scalar(loc) => {
+                    unresolved_locations.push((loc.uri, loc.range));
+                }
+                lsp::GotoDefinitionResponse::Array(locs) => {
+                    unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range)));
+                }
+                lsp::GotoDefinitionResponse::Link(links) => {
+                    unresolved_locations.extend(
+                        links
+                            .into_iter()
+                            .map(|l| (l.target_uri, l.target_selection_range)),
+                    );
+                }
+            }
+
+            for (target_uri, target_range) in unresolved_locations {
+                let target_buffer_handle = project
+                    .update(&mut cx, |this, cx| {
+                        this.open_local_buffer_from_lsp_path(
+                            target_uri,
+                            language.name().to_string(),
+                            language_server.clone(),
+                            cx,
+                        )
+                    })
+                    .await?;
+
+                cx.read(|cx| {
+                    let target_buffer = target_buffer_handle.read(cx);
+                    let target_start = target_buffer
+                        .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
+                    let target_end = target_buffer
+                        .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
+                    definitions.push(Definition {
+                        target_buffer: target_buffer_handle,
+                        target_range: target_buffer.anchor_after(target_start)
+                            ..target_buffer.anchor_before(target_end),
+                    });
+                });
+            }
+        }
+
+        Ok(definitions)
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
+        proto::GetDefinition {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+        }
+    }
+
+    fn from_proto(message: proto::GetDefinition, _: &mut Project, buffer: &Buffer) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        if !buffer.can_resolve(&position) {
+            Err(anyhow!("cannot resolve position"))?;
+        }
+        Ok(Self {
+            position: position.to_point_utf16(buffer),
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<Definition>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &AppContext,
+    ) -> proto::GetDefinitionResponse {
+        let definitions = response
+            .into_iter()
+            .map(|definition| {
+                let buffer =
+                    project.serialize_buffer_for_peer(&definition.target_buffer, peer_id, cx);
+                proto::Definition {
+                    target_start: Some(serialize_anchor(&definition.target_range.start)),
+                    target_end: Some(serialize_anchor(&definition.target_range.end)),
+                    buffer: Some(buffer),
+                }
+            })
+            .collect();
+        proto::GetDefinitionResponse { definitions }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDefinitionResponse,
+        project: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<Definition>> {
+        let mut definitions = Vec::new();
+        for definition in message.definitions {
+            let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
+            let target_buffer = project
+                .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                .await?;
+            let target_start = definition
+                .target_start
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target start"))?;
+            let target_end = definition
+                .target_end
+                .and_then(deserialize_anchor)
+                .ok_or_else(|| anyhow!("missing target end"))?;
+            definitions.push(Definition {
+                target_buffer,
+                target_range: target_start..target_end,
+            })
+        }
+        Ok(definitions)
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {
+        message.buffer_id
+    }
+}

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};
@@ -13,13 +14,12 @@ use gpui::{
     UpgradeModelHandle, WeakModelHandle,
 };
 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::{
@@ -181,7 +181,9 @@ impl Project {
         client.add_entity_request_handler(Self::handle_format_buffers);
         client.add_entity_request_handler(Self::handle_get_code_actions);
         client.add_entity_request_handler(Self::handle_get_completions);
-        client.add_entity_request_handler(Self::handle_get_definition);
+        client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
+        client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
         client.add_entity_request_handler(Self::handle_open_buffer);
         client.add_entity_request_handler(Self::handle_save_buffer);
     }
@@ -1171,137 +1173,12 @@ impl Project {
 
     pub fn definition<T: ToPointUtf16>(
         &self,
-        source_buffer_handle: &ModelHandle<Buffer>,
+        buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Definition>>> {
-        let source_buffer_handle = source_buffer_handle.clone();
-        let source_buffer = source_buffer_handle.read(cx);
-        let worktree;
-        let buffer_abs_path;
-        if let Some(file) = File::from_dyn(source_buffer.file()) {
-            worktree = file.worktree.clone();
-            buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
-        } else {
-            return Task::ready(Ok(Default::default()));
-        };
-
-        let position = position.to_point_utf16(source_buffer);
-
-        if worktree.read(cx).as_local().is_some() {
-            let buffer_abs_path = buffer_abs_path.unwrap();
-            let lang_name;
-            let lang_server;
-            if let Some(lang) = source_buffer.language() {
-                lang_name = lang.name().to_string();
-                if let Some(server) = self
-                    .language_servers
-                    .get(&(worktree.read(cx).id(), lang_name.clone()))
-                {
-                    lang_server = server.clone();
-                } else {
-                    return Task::ready(Ok(Default::default()));
-                };
-            } else {
-                return Task::ready(Ok(Default::default()));
-            }
-
-            cx.spawn(|this, mut cx| async move {
-                let response = lang_server
-                    .request::<lsp::request::GotoDefinition>(lsp::GotoDefinitionParams {
-                        text_document_position_params: lsp::TextDocumentPositionParams {
-                            text_document: lsp::TextDocumentIdentifier::new(
-                                lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
-                            ),
-                            position: lsp::Position::new(position.row, position.column),
-                        },
-                        work_done_progress_params: Default::default(),
-                        partial_result_params: Default::default(),
-                    })
-                    .await?;
-
-                let mut definitions = Vec::new();
-                if let Some(response) = response {
-                    let mut unresolved_locations = Vec::new();
-                    match response {
-                        lsp::GotoDefinitionResponse::Scalar(loc) => {
-                            unresolved_locations.push((loc.uri, loc.range));
-                        }
-                        lsp::GotoDefinitionResponse::Array(locs) => {
-                            unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range)));
-                        }
-                        lsp::GotoDefinitionResponse::Link(links) => {
-                            unresolved_locations.extend(
-                                links
-                                    .into_iter()
-                                    .map(|l| (l.target_uri, l.target_selection_range)),
-                            );
-                        }
-                    }
-
-                    for (target_uri, target_range) in unresolved_locations {
-                        let target_buffer_handle = this
-                            .update(&mut cx, |this, cx| {
-                                this.open_local_buffer_from_lsp_path(
-                                    target_uri,
-                                    lang_name.clone(),
-                                    lang_server.clone(),
-                                    cx,
-                                )
-                            })
-                            .await?;
-
-                        cx.read(|cx| {
-                            let target_buffer = target_buffer_handle.read(cx);
-                            let target_start = target_buffer
-                                .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
-                            let target_end = target_buffer
-                                .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
-                            definitions.push(Definition {
-                                target_buffer: target_buffer_handle,
-                                target_range: target_buffer.anchor_after(target_start)
-                                    ..target_buffer.anchor_before(target_end),
-                            });
-                        });
-                    }
-                }
-
-                Ok(definitions)
-            })
-        } else if let Some(project_id) = self.remote_id() {
-            let client = self.client.clone();
-            let request = proto::GetDefinition {
-                project_id,
-                buffer_id: source_buffer.remote_id(),
-                position: Some(serialize_anchor(&source_buffer.anchor_before(position))),
-            };
-            cx.spawn(|this, mut cx| async move {
-                let response = client.request(request).await?;
-                let mut definitions = Vec::new();
-                for definition in response.definitions {
-                    let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
-                    let target_buffer = this
-                        .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
-                        .await?;
-                    let target_start = definition
-                        .target_start
-                        .and_then(deserialize_anchor)
-                        .ok_or_else(|| anyhow!("missing target start"))?;
-                    let target_end = definition
-                        .target_end
-                        .and_then(deserialize_anchor)
-                        .ok_or_else(|| anyhow!("missing target end"))?;
-                    definitions.push(Definition {
-                        target_buffer,
-                        target_range: target_start..target_end,
-                    })
-                }
-
-                Ok(definitions)
-            })
-        } else {
-            Task::ready(Ok(Default::default()))
-        }
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
     }
 
     pub fn completions<T: ToPointUtf16>(
@@ -1625,7 +1502,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 +1532,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 +1569,199 @@ 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, PrepareRename { position }, cx)
+    }
+
+    pub fn perform_rename<T: ToPointUtf16>(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        position: T,
+        new_name: String,
+        push_to_history: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(
+            buffer,
+            PerformRename {
+                position,
+                new_name,
+                push_to_history,
+            },
+            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, buffer_handle, cx)
+                        .await
+                });
+            }
+        } else if let Some(project_id) = self.remote_id() {
+            let rpc = self.client.clone();
+            let message = request.to_proto(project_id, buffer);
+            return cx.spawn(|this, cx| async move {
+                let response = rpc.request(message).await?;
+                request
+                    .response_from_proto(response, this, buffer_handle, cx)
+                    .await
+            });
+        }
+        Task::ready(Ok(Default::default()))
+    }
+
     pub fn find_or_create_local_worktree(
         &self,
         abs_path: impl AsRef<Path>,
@@ -2489,47 +2451,37 @@ impl Project {
         })
     }
 
-    async fn handle_get_definition(
+    async fn handle_lsp_command<T: LspCommand>(
         this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::GetDefinition>,
+        envelope: TypedEnvelope<T::ProtoRequest>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
-    ) -> Result<proto::GetDefinitionResponse> {
+    ) -> Result<<T::ProtoRequest as proto::RequestMessage>::Response>
+    where
+        <T::LspRequest as lsp::request::Request>::Result: Send,
+    {
         let sender_id = envelope.original_sender_id()?;
-        let position = envelope
-            .payload
-            .position
-            .and_then(deserialize_anchor)
-            .ok_or_else(|| anyhow!("invalid position"))?;
-        let definitions = this.update(&mut cx, |this, cx| {
-            let source_buffer = this
+        let (request, buffer_version) = this.update(&mut cx, |this, cx| {
+            let buffer_id = T::buffer_id_from_proto(&envelope.payload);
+            let buffer_handle = this
                 .shared_buffers
                 .get(&sender_id)
-                .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned())
-                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
-            if source_buffer.read(cx).can_resolve(&position) {
-                Ok(this.definition(&source_buffer, position, cx))
-            } else {
-                Err(anyhow!("cannot resolve position"))
-            }
+                .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned())
+                .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?;
+            let buffer = buffer_handle.read(cx);
+            let buffer_version = buffer.version();
+            let request = T::from_proto(envelope.payload, this, buffer)?;
+            Ok::<_, anyhow::Error>((this.request_lsp(buffer_handle, request, cx), buffer_version))
         })?;
-
-        let definitions = definitions.await?;
-
+        let response = request.await?;
         this.update(&mut cx, |this, cx| {
-            let mut response = proto::GetDefinitionResponse {
-                definitions: Default::default(),
-            };
-            for definition in definitions {
-                let buffer =
-                    this.serialize_buffer_for_peer(&definition.target_buffer, sender_id, cx);
-                response.definitions.push(proto::Definition {
-                    target_start: Some(serialize_anchor(&definition.target_range.start)),
-                    target_end: Some(serialize_anchor(&definition.target_range.end)),
-                    buffer: Some(buffer),
-                });
-            }
-            Ok(response)
+            Ok(T::response_to_proto(
+                response,
+                this,
+                sender_id,
+                &buffer_version,
+                cx,
+            ))
         })
     }
 
@@ -2980,13 +2932,11 @@ impl From<lsp::DeleteFileOptions> for fs::RemoveOptions {
 #[cfg(test)]
 mod tests {
     use super::{Event, *};
-    use client::test::FakeHttpClient;
     use fs::RealFs;
     use futures::StreamExt;
     use gpui::test::subscribe;
     use language::{
-        tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageRegistry,
-        LanguageServerConfig, Point,
+        tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageServerConfig, Point,
     };
     use lsp::Url;
     use serde_json::json;
@@ -3066,8 +3016,7 @@ mod tests {
             .clone()
             .unwrap();
 
-        let mut languages = LanguageRegistry::new();
-        languages.add(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".to_string(),
                 path_suffixes: vec!["rs".to_string()],
@@ -3075,30 +3024,26 @@ mod tests {
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
-        let dir = temp_tree(json!({
-            "a.rs": "fn a() { A }",
-            "b.rs": "const y: i32 = 1",
-        }));
-
-        let http_client = FakeHttpClient::with_404_response();
-        let client = Client::new(http_client.clone());
-        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": "fn a() { A }",
+                "b.rs": "const y: i32 = 1",
+            }),
+        )
+        .await;
 
-        let project = cx.update(|cx| {
-            Project::local(
-                client,
-                user_store,
-                Arc::new(languages),
-                Arc::new(RealFs),
-                cx,
-            )
+        let project = Project::test(fs, &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.path(), false, cx)
+                project.find_or_create_local_worktree("/dir", false, cx)
             })
             .await
             .unwrap();
@@ -3110,13 +3055,7 @@ mod tests {
         // Cause worktree to start the fake language server
         let _buffer = project
             .update(&mut cx, |project, cx| {
-                project.open_buffer(
-                    ProjectPath {
-                        worktree_id,
-                        path: Path::new("b.rs").into(),
-                    },
-                    cx,
-                )
+                project.open_buffer((worktree_id, Path::new("b.rs")), cx)
             })
             .await
             .unwrap();
@@ -3136,7 +3075,7 @@ mod tests {
 
         fake_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
-                uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(),
+                uri: Url::from_file_path("/dir/a.rs").unwrap(),
                 version: None,
                 diagnostics: vec![lsp::Diagnostic {
                     range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
@@ -3148,10 +3087,7 @@ mod tests {
             .await;
         assert_eq!(
             events.next().await.unwrap(),
-            Event::DiagnosticsUpdated(ProjectPath {
-                worktree_id,
-                path: Arc::from(Path::new("a.rs"))
-            })
+            Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into())
         );
 
         fake_server.end_progress(&progress_token).await;
@@ -3226,9 +3162,7 @@ mod tests {
     #[gpui::test]
     async fn test_definition(mut cx: gpui::TestAppContext) {
         let (language_server_config, mut fake_servers) = LanguageServerConfig::fake();
-
-        let mut languages = LanguageRegistry::new();
-        languages.add(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig {
                 name: "Rust".to_string(),
                 path_suffixes: vec!["rs".to_string()],
@@ -3236,30 +3170,26 @@ mod tests {
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
-        let dir = temp_tree(json!({
-            "a.rs": "const fn a() { A }",
-            "b.rs": "const y: i32 = crate::a()",
-        }));
-        let dir_path = dir.path().to_path_buf();
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": "const fn a() { A }",
+                "b.rs": "const y: i32 = crate::a()",
+            }),
+        )
+        .await;
 
-        let http_client = FakeHttpClient::with_404_response();
-        let client = Client::new(http_client.clone());
-        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let project = cx.update(|cx| {
-            Project::local(
-                client,
-                user_store,
-                Arc::new(languages),
-                Arc::new(RealFs),
-                cx,
-            )
+        let project = Project::test(fs, &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.path().join("b.rs"), false, cx)
+                project.find_or_create_local_worktree("/dir/b.rs", false, cx)
             })
             .await
             .unwrap();
@@ -3285,12 +3215,12 @@ mod tests {
             let params = params.text_document_position_params;
             assert_eq!(
                 params.text_document.uri.to_file_path().unwrap(),
-                dir_path.join("b.rs")
+                Path::new("/dir/b.rs"),
             );
             assert_eq!(params.position, lsp::Position::new(0, 22));
 
             Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                lsp::Url::from_file_path(dir_path.join("a.rs")).unwrap(),
+                lsp::Url::from_file_path("/dir/a.rs").unwrap(),
                 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
             )))
         });
@@ -3311,15 +3241,12 @@ mod tests {
                     .as_local()
                     .unwrap()
                     .abs_path(cx),
-                dir.path().join("a.rs")
+                Path::new("/dir/a.rs"),
             );
             assert_eq!(definition.target_range.to_offset(target_buffer), 9..10);
             assert_eq!(
                 list_worktrees(&project, cx),
-                [
-                    (dir.path().join("b.rs"), false),
-                    (dir.path().join("a.rs"), true)
-                ]
+                [("/dir/b.rs".as_ref(), false), ("/dir/a.rs".as_ref(), true)]
             );
 
             drop(definition);
@@ -3327,18 +3254,21 @@ mod tests {
         cx.read(|cx| {
             assert_eq!(
                 list_worktrees(&project, cx),
-                [(dir.path().join("b.rs"), false)]
+                [("/dir/b.rs".as_ref(), false)]
             );
         });
 
-        fn list_worktrees(project: &ModelHandle<Project>, cx: &AppContext) -> Vec<(PathBuf, bool)> {
+        fn list_worktrees<'a>(
+            project: &'a ModelHandle<Project>,
+            cx: &'a AppContext,
+        ) -> Vec<(&'a Path, bool)> {
             project
                 .read(cx)
                 .worktrees(cx)
                 .map(|worktree| {
                     let worktree = worktree.read(cx);
                     (
-                        worktree.as_local().unwrap().abs_path().to_path_buf(),
+                        worktree.as_local().unwrap().abs_path().as_ref(),
                         worktree.is_weak(),
                     )
                 })
@@ -3348,7 +3278,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_save_file(mut cx: gpui::TestAppContext) {
-        let fs = Arc::new(FakeFs::new(cx.background()));
+        let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/dir",
             json!({
@@ -3386,7 +3316,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_save_in_single_file_worktree(mut cx: gpui::TestAppContext) {
-        let fs = Arc::new(FakeFs::new(cx.background()));
+        let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/dir",
             json!({
@@ -3576,7 +3506,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_buffer_deduping(mut cx: gpui::TestAppContext) {
-        let fs = Arc::new(FakeFs::new(cx.background()));
+        let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/the-dir",
             json!({
@@ -3865,7 +3795,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
-        let fs = Arc::new(FakeFs::new(cx.background()));
+        let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/the-dir",
             json!({
@@ -4121,4 +4051,146 @@ 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);
+
+        let response = project.update(&mut cx, |project, cx| {
+            project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
+        });
+        fake_server
+            .handle_request::<lsp::request::Rename, _>(|params| {
+                assert_eq!(
+                    params.text_document_position.text_document.uri.as_str(),
+                    "file:///dir/one.rs"
+                );
+                assert_eq!(
+                    params.text_document_position.position,
+                    lsp::Position::new(0, 7)
+                );
+                assert_eq!(params.new_name, "THREE");
+                Some(lsp::WorkspaceEdit {
+                    changes: Some(
+                        [
+                            (
+                                lsp::Url::from_file_path("/dir/one.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 6),
+                                        lsp::Position::new(0, 9),
+                                    ),
+                                    "THREE".to_string(),
+                                )],
+                            ),
+                            (
+                                lsp::Url::from_file_path("/dir/two.rs").unwrap(),
+                                vec![
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 24),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 35),
+                                            lsp::Position::new(0, 38),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                ],
+                            ),
+                        ]
+                        .into_iter()
+                        .collect(),
+                    ),
+                    ..Default::default()
+                })
+            })
+            .next()
+            .await
+            .unwrap();
+        let mut transaction = response.await.unwrap().0;
+        assert_eq!(transaction.len(), 2);
+        assert_eq!(
+            transaction
+                .remove_entry(&buffer)
+                .unwrap()
+                .0
+                .read_with(&cx, |buffer, _| buffer.text()),
+            "const THREE: usize = 1;"
+        );
+        assert_eq!(
+            transaction
+                .into_keys()
+                .next()
+                .unwrap()
+                .read_with(&cx, |buffer, _| buffer.text()),
+            "const TWO: usize = one::THREE + one::THREE;"
+        );
+    }
 }

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,8 @@ entity_messages!(
     JoinProject,
     LeaveProject,
     OpenBuffer,
+    PerformRename,
+    PrepareRename,
     RemoveProjectCollaborator,
     SaveBuffer,
     ShareWorktree,

crates/server/src/rpc.rs 🔗

@@ -91,6 +91,8 @@ impl Server {
             .add_request_handler(Server::apply_additional_edits_for_completion)
             .add_request_handler(Server::get_code_actions)
             .add_request_handler(Server::apply_code_action)
+            .add_request_handler(Server::prepare_rename)
+            .add_request_handler(Server::perform_rename)
             .add_request_handler(Server::get_channels)
             .add_request_handler(Server::get_users)
             .add_request_handler(Server::join_channel)
@@ -708,6 +710,34 @@ impl Server {
             .await?)
     }
 
+    async fn prepare_rename(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::PrepareRename>,
+    ) -> tide::Result<proto::PrepareRenameResponse> {
+        let host = self
+            .state()
+            .read_project(request.payload.project_id, request.sender_id)?
+            .host_connection_id;
+        Ok(self
+            .peer
+            .forward_request(request.sender_id, host, request.payload.clone())
+            .await?)
+    }
+
+    async fn perform_rename(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::PerformRename>,
+    ) -> tide::Result<proto::PerformRenameResponse> {
+        let host = self
+            .state()
+            .read_project(request.payload.project_id, request.sender_id)?
+            .host_connection_id;
+        Ok(self
+            .peer
+            .forward_request(request.sender_id, host, request.payload.clone())
+            .await?)
+    }
+
     async fn update_buffer(
         self: Arc<Server>,
         request: TypedEnvelope<proto::UpdateBuffer>,
@@ -1122,8 +1152,8 @@ mod tests {
             EstablishConnectionError, UserStore,
         },
         editor::{
-            self, ConfirmCodeAction, ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer,
-            Redo, ToggleCodeActions, Undo,
+            self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings,
+            Input, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo,
         },
         fs::{FakeFs, Fs as _},
         language::{
@@ -1147,7 +1177,7 @@ mod tests {
     async fn test_share_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         let (window_b, _) = cx_b.add_window(|_| EmptyView);
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
@@ -1285,7 +1315,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_unshare_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 2 clients.
@@ -1386,7 +1416,7 @@ mod tests {
         mut cx_c: TestAppContext,
     ) {
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         cx_a.foreground().forbid_parking();
 
         // Connect to a server as 3 clients.
@@ -1514,9 +1544,7 @@ mod tests {
         fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
             .await
             .unwrap();
-        fs.insert_file(Path::new("/a/file4"), "4".into())
-            .await
-            .unwrap();
+        fs.insert_file(Path::new("/a/file4"), "4".into()).await;
 
         worktree_a
             .condition(&cx_a, |tree, _| {
@@ -1565,7 +1593,7 @@ mod tests {
     async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -1653,7 +1681,7 @@ mod tests {
     async fn test_buffer_reloading(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -1738,7 +1766,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -1820,7 +1848,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -1895,7 +1923,7 @@ mod tests {
     async fn test_peer_disconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -1969,7 +1997,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
@@ -2193,7 +2221,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
         let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
@@ -2402,7 +2430,7 @@ mod tests {
     async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
@@ -2504,7 +2532,7 @@ mod tests {
     async fn test_definition(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/root-1",
             json!({
@@ -2657,7 +2685,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/root",
             json!({
@@ -2766,7 +2794,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let mut lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
         let mut path_openers_b = Vec::new();
         cx_b.update(|cx| editor::init(cx, &mut path_openers_b));
 
@@ -3001,6 +3029,223 @@ mod tests {
         });
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_collaborating_with_renames(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let mut lang_registry = Arc::new(LanguageRegistry::new());
+        let fs = FakeFs::new(cx_a.background());
+        let mut path_openers_b = Vec::new();
+        cx_b.update(|cx| editor::init(cx, &mut path_openers_b));
+
+        // Set up a fake language server.
+        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
+        Arc::get_mut(&mut lang_registry)
+            .unwrap()
+            .add(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()),
+            )));
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let client_a = server.create_client(&mut cx_a, "user_a").await;
+        let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+        // Share a project as client A
+        fs.insert_tree(
+            "/dir",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "one.rs": "const ONE: usize = 1;",
+                "two.rs": "const TWO: usize = one::ONE + one::ONE;"
+            }),
+        )
+        .await;
+        let project_a = cx_a.update(|cx| {
+            Project::local(
+                client_a.clone(),
+                client_a.user_store.clone(),
+                lang_registry.clone(),
+                fs.clone(),
+                cx,
+            )
+        });
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_local_worktree("/dir", false, cx)
+            })
+            .await
+            .unwrap();
+        worktree_a
+            .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
+        let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
+        project_a
+            .update(&mut cx_a, |p, cx| p.share(cx))
+            .await
+            .unwrap();
+
+        // Join the worktree as client B.
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+        let mut params = cx_b.update(WorkspaceParams::test);
+        params.languages = lang_registry.clone();
+        params.client = client_b.client.clone();
+        params.user_store = client_b.user_store.clone();
+        params.project = project_b;
+        params.path_openers = path_openers_b.into();
+
+        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
+        let editor_b = workspace_b
+            .update(&mut cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "one.rs").into(), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+
+        // Move cursor to a location that can be renamed.
+        let prepare_rename = editor_b.update(&mut cx_b, |editor, cx| {
+            editor.select_ranges([7..7], None, cx);
+            editor.rename(&Rename, cx).unwrap()
+        });
+
+        fake_language_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();
+        prepare_rename.await.unwrap();
+        editor_b.update(&mut cx_b, |editor, cx| {
+            let rename = editor.pending_rename().unwrap();
+            let buffer = editor.buffer().read(cx).snapshot(cx);
+            assert_eq!(
+                rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
+                6..9
+            );
+            rename.editor.update(cx, |rename_editor, cx| {
+                rename_editor.buffer().update(cx, |rename_buffer, cx| {
+                    rename_buffer.edit([0..3], "THREE", cx);
+                });
+            });
+        });
+
+        let confirm_rename = workspace_b.update(&mut cx_b, |workspace, cx| {
+            Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
+        });
+        fake_language_server
+            .handle_request::<lsp::request::Rename, _>(|params| {
+                assert_eq!(
+                    params.text_document_position.text_document.uri.as_str(),
+                    "file:///dir/one.rs"
+                );
+                assert_eq!(
+                    params.text_document_position.position,
+                    lsp::Position::new(0, 6)
+                );
+                assert_eq!(params.new_name, "THREE");
+                Some(lsp::WorkspaceEdit {
+                    changes: Some(
+                        [
+                            (
+                                lsp::Url::from_file_path("/dir/one.rs").unwrap(),
+                                vec![lsp::TextEdit::new(
+                                    lsp::Range::new(
+                                        lsp::Position::new(0, 6),
+                                        lsp::Position::new(0, 9),
+                                    ),
+                                    "THREE".to_string(),
+                                )],
+                            ),
+                            (
+                                lsp::Url::from_file_path("/dir/two.rs").unwrap(),
+                                vec![
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 24),
+                                            lsp::Position::new(0, 27),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                    lsp::TextEdit::new(
+                                        lsp::Range::new(
+                                            lsp::Position::new(0, 35),
+                                            lsp::Position::new(0, 38),
+                                        ),
+                                        "THREE".to_string(),
+                                    ),
+                                ],
+                            ),
+                        ]
+                        .into_iter()
+                        .collect(),
+                    ),
+                    ..Default::default()
+                })
+            })
+            .next()
+            .await
+            .unwrap();
+        confirm_rename.await.unwrap();
+
+        let rename_editor = workspace_b.read_with(&cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+        rename_editor.update(&mut cx_b, |editor, cx| {
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;"
+            );
+            editor.undo(&Undo, cx);
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::ONE + one::ONE;\nconst ONE: usize = 1;"
+            );
+            editor.redo(&Redo, cx);
+            assert_eq!(
+                editor.text(cx),
+                "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;"
+            );
+        });
+
+        // Ensure temporary rename edits cannot be undone/redone.
+        editor_b.update(&mut cx_b, |editor, cx| {
+            editor.undo(&Undo, cx);
+            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+            editor.undo(&Undo, cx);
+            assert_eq!(editor.text(cx), "const ONE: usize = 1;");
+            editor.redo(&Redo, cx);
+            assert_eq!(editor.text(cx), "const THREE: usize = 1;");
+        })
+    }
+
     #[gpui::test(iterations = 10)]
     async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();
@@ -3421,7 +3666,7 @@ mod tests {
     ) {
         cx_a.foreground().forbid_parking();
         let lang_registry = Arc::new(LanguageRegistry::new());
-        let fs = Arc::new(FakeFs::new(cx_a.background()));
+        let fs = FakeFs::new(cx_a.background());
 
         // Connect to a server as 3 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3591,6 +3836,13 @@ mod tests {
                     },
                 )])
             });
+
+            fake_server.handle_request::<lsp::request::PrepareRenameRequest, _>(|params| {
+                Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                    params.position,
+                    params.position,
+                )))
+            });
         });
 
         Arc::get_mut(&mut host_lang_registry)
@@ -3605,7 +3857,7 @@ mod tests {
                 None,
             )));
 
-        let fs = Arc::new(FakeFs::new(cx.background()));
+        let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/_collab",
             json!({
@@ -4223,6 +4475,26 @@ mod tests {
                             save.await;
                         }
                     }
+                    40..=45 => {
+                        let prepare_rename = project.update(&mut cx, |project, cx| {
+                            log::info!(
+                                "Guest {}: preparing rename for buffer {:?}",
+                                guest_id,
+                                buffer.read(cx).file().unwrap().full_path(cx)
+                            );
+                            let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len());
+                            project.prepare_rename(buffer, offset, cx)
+                        });
+                        let prepare_rename = cx.background().spawn(async move {
+                            prepare_rename.await.expect("prepare rename request failed");
+                        });
+                        if rng.borrow_mut().gen_bool(0.3) {
+                            log::info!("Guest {}: detaching prepare rename request", guest_id);
+                            prepare_rename.detach();
+                        } else {
+                            prepare_rename.await;
+                        }
+                    }
                     _ => {
                         buffer.update(&mut cx, |buffer, cx| {
                             log::info!(

crates/text/src/text.rs 🔗

@@ -1222,7 +1222,6 @@ impl Buffer {
             .iter()
             .map(|entry| entry.transaction.clone())
             .collect::<Vec<_>>();
-
         transactions
             .into_iter()
             .map(|transaction| self.undo_or_redo(transaction).unwrap())
@@ -1251,7 +1250,6 @@ impl Buffer {
             .iter()
             .map(|entry| entry.transaction.clone())
             .collect::<Vec<_>>();
-
         transactions
             .into_iter()
             .map(|transaction| self.undo_or_redo(transaction).unwrap())

crates/theme/src/theme.rs 🔗

@@ -278,6 +278,8 @@ pub struct EditorStyle {
     pub gutter_padding_factor: f32,
     pub active_line_background: Color,
     pub highlighted_line_background: Color,
+    pub diff_background_deleted: Color,
+    pub diff_background_inserted: Color,
     pub line_number: Color,
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
@@ -383,6 +385,8 @@ impl InputEditorStyle {
             gutter_padding_factor: Default::default(),
             active_line_background: Default::default(),
             highlighted_line_background: Default::default(),
+            diff_background_deleted: Default::default(),
+            diff_background_inserted: Default::default(),
             line_number: Default::default(),
             line_number_active: Default::default(),
             guest_selections: Default::default(),

crates/workspace/src/workspace.rs 🔗

@@ -492,7 +492,7 @@ pub struct WorkspaceParams {
 impl WorkspaceParams {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut MutableAppContext) -> Self {
-        let fs = Arc::new(project::FakeFs::new(cx.background().clone()));
+        let fs = project::FakeFs::new(cx.background().clone());
         let languages = Arc::new(LanguageRegistry::new());
         let http_client = client::test::FakeHttpClient::new(|_| async move {
             Ok(client::http::ServerResponse::new(404))

crates/zed/assets/themes/_base.toml 🔗

@@ -188,7 +188,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -248,6 +248,8 @@ gutter_background = "$surface.1"
 gutter_padding_factor = 2.5
 active_line_background = "$state.active_line"
 highlighted_line_background = "$state.highlighted_line"
+diff_background_deleted = "$state.deleted_line"
+diff_background_inserted = "$state.inserted_line"
 line_number = "$text.2.color"
 line_number_active = "$text.0.color"
 selection = "$selection.host"

crates/zed/assets/themes/black.toml 🔗

@@ -19,7 +19,7 @@ extends = "_base"
 0 = "#00000052"
 
 [selection]
-host = { selection = "#3B57BC33", cursor = "$text.0.color" }
+host = { selection = "#3B57BC55", cursor = "$text.0.color" }
 guests = [
   { selection = "#FDF35133", cursor = "#FDF351" },
   { selection = "#4EACAD33", cursor = "#4EACAD" },
@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#161313"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000036"
+inserted_line = "#00dd0036"
 hover = "#00000033"
 selected = "#00000088"
 

crates/zed/assets/themes/dark.toml 🔗

@@ -19,7 +19,7 @@ extends = "_base"
 0 = "#00000052"
 
 [selection]
-host = { selection = "#3B57BC33", cursor = "$text.0.color" }
+host = { selection = "#3B57BC55", cursor = "$text.0.color" }
 guests = [
   { selection = "#FDF35133", cursor = "#FDF351" },
   { selection = "#4EACAD33", cursor = "#4EACAD" },
@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#00000022"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000036"
+inserted_line = "#00dd0036"
 hover = "#00000033"
 selected = "#00000088"
 

crates/zed/assets/themes/light.toml 🔗

@@ -19,7 +19,7 @@ extends = "_base"
 0 = "#0000000D"
 
 [selection]
-host = { selection = "#3B57BC33", cursor = "$text.0.color" }
+host = { selection = "#3B57BC55", cursor = "$text.0.color" }
 guests = [
   { selection = "#D0453B33", cursor = "#D0453B" },
   { selection = "#3B874B33", cursor = "#3B874B" },
@@ -39,6 +39,8 @@ bad = "#b7372e"
 [state]
 active_line = "#00000008"
 highlighted_line = "#faca5033"
+deleted_line = "#dd000036"
+inserted_line = "#00dd0036"
 hover = "#0000000D"
 selected = "#0000001c"
 

crates/zed/src/test.rs 🔗

@@ -42,7 +42,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
         channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
         client,
         user_store,
-        fs: Arc::new(FakeFs::new(cx.background().clone())),
+        fs: FakeFs::new(cx.background().clone()),
         path_openers: Arc::from(path_openers),
         build_window_options: &build_window_options,
         build_workspace: &build_workspace,

crates/zed/src/zed.rs 🔗

@@ -214,7 +214,7 @@ mod tests {
         });
 
         let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
-        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
+        app_state.fs.as_fake().insert_dir("/root").await;
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
         save_task.await.unwrap();
         editor.read_with(&cx, |editor, cx| {
@@ -348,10 +348,10 @@ mod tests {
     async fn test_open_paths(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
         let fs = app_state.fs.as_fake();
-        fs.insert_dir("/dir1").await.unwrap();
-        fs.insert_dir("/dir2").await.unwrap();
-        fs.insert_file("/dir1/a.txt", "".into()).await.unwrap();
-        fs.insert_file("/dir2/b.txt", "".into()).await.unwrap();
+        fs.insert_dir("/dir1").await;
+        fs.insert_dir("/dir2").await;
+        fs.insert_file("/dir1/a.txt", "".into()).await;
+        fs.insert_file("/dir2/b.txt", "".into()).await;
 
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
@@ -456,9 +456,7 @@ mod tests {
                 editor.handle_input(&editor::Input("x".into()), cx)
             })
         });
-        fs.insert_file("/root/a.txt", "changed".to_string())
-            .await
-            .unwrap();
+        fs.insert_file("/root/a.txt", "changed".to_string()).await;
         editor
             .condition(&cx, |editor, cx| editor.has_conflict(cx))
             .await;
@@ -476,7 +474,7 @@ mod tests {
     #[gpui::test]
     async fn test_open_and_save_new_file(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
-        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
+        app_state.fs.as_fake().insert_dir("/root").await;
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         params
@@ -576,7 +574,7 @@ mod tests {
     #[gpui::test]
     async fn test_setting_language_when_saving_as_single_file_worktree(mut cx: TestAppContext) {
         let app_state = cx.update(test_app_state);
-        app_state.fs.as_fake().insert_dir("/root").await.unwrap();
+        app_state.fs.as_fake().insert_dir("/root").await;
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));