Fix some issues with branch buffers (#18945)

Max Brunsfeld created

* `Open Excerpts` command always opens the locations in the base buffer
* LSP features like document-highlights, go-to-def, and inlay hints work
correctly in branch buffers
* Other LSP features like completions, code actions, and rename are
disabled in branch buffers

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs           |   2 
crates/assistant/src/prompt_library.rs            |   4 
crates/collab_ui/src/chat_panel/message_editor.rs |   2 
crates/editor/src/editor.rs                       | 348 ++++++++++++----
crates/editor/src/hover_links.rs                  | 102 ++--
crates/editor/src/hover_popover.rs                |  47 -
crates/editor/src/inlay_hint_cache.rs             |  73 +-
crates/editor/src/proposed_changes_editor.rs      | 119 +++++
crates/language/src/buffer_tests.rs               |  82 ++-
crates/project/src/project.rs                     |   4 
crates/project/src/project_tests.rs               |   2 
crates/remote_server/src/remote_editing_tests.rs  |   2 
crates/text/src/text.rs                           |  36 +
13 files changed, 555 insertions(+), 268 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -1560,7 +1560,7 @@ impl ContextEditor {
             editor.set_show_runnables(false, cx);
             editor.set_show_wrap_guides(false, cx);
             editor.set_show_indent_guides(false, cx);
-            editor.set_completion_provider(Box::new(completion_provider));
+            editor.set_completion_provider(Some(Box::new(completion_provider)));
             editor.set_collaboration_hub(Box::new(project.clone()));
             editor
         });

crates/assistant/src/prompt_library.rs 🔗

@@ -521,9 +521,9 @@ impl PromptLibrary {
                             editor.set_show_indent_guides(false, cx);
                             editor.set_use_modal_editing(false);
                             editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
-                            editor.set_completion_provider(Box::new(
+                            editor.set_completion_provider(Some(Box::new(
                                 SlashCommandCompletionProvider::new(None, None),
-                            ));
+                            )));
                             if focus {
                                 editor.focus(cx);
                             }

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -111,7 +111,7 @@ impl MessageEditor {
             editor.set_show_gutter(false, cx);
             editor.set_show_wrap_guides(false, cx);
             editor.set_show_indent_guides(false, cx);
-            editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
+            editor.set_completion_provider(Some(Box::new(MessageEditorCompletionProvider(this))));
             editor.set_auto_replace_emoji_shortcode(
                 MessageEditorSettings::get_global(cx)
                     .auto_replace_emoji_shortcode

crates/editor/src/editor.rs 🔗

@@ -121,10 +121,11 @@ use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
-use project::project_settings::{GitGutterSetting, ProjectSettings};
 use project::{
-    lsp_store::FormatTrigger, CodeAction, Completion, CompletionIntent, Item, Location, Project,
-    ProjectPath, ProjectTransaction, TaskSourceKind,
+    lsp_store::FormatTrigger,
+    project_settings::{GitGutterSetting, ProjectSettings},
+    CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
+    LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
 };
 use rand::prelude::*;
 use rpc::{proto::*, ErrorExt};
@@ -546,6 +547,7 @@ pub struct Editor {
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     soft_wrap_mode_override: Option<language_settings::SoftWrap>,
     project: Option<Model<Project>>,
+    semantics_provider: Option<Rc<dyn SemanticsProvider>>,
     completion_provider: Option<Box<dyn CompletionProvider>>,
     collaboration_hub: Option<Box<dyn CollaborationHub>>,
     blink_manager: Model<BlinkManager>,
@@ -884,12 +886,12 @@ enum ContextMenu {
 impl ContextMenu {
     fn select_first(
         &mut self,
-        project: Option<&Model<Project>>,
+        provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_first(project, cx),
+                ContextMenu::Completions(menu) => menu.select_first(provider, cx),
                 ContextMenu::CodeActions(menu) => menu.select_first(cx),
             }
             true
@@ -900,12 +902,12 @@ impl ContextMenu {
 
     fn select_prev(
         &mut self,
-        project: Option<&Model<Project>>,
+        provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_prev(project, cx),
+                ContextMenu::Completions(menu) => menu.select_prev(provider, cx),
                 ContextMenu::CodeActions(menu) => menu.select_prev(cx),
             }
             true
@@ -916,12 +918,12 @@ impl ContextMenu {
 
     fn select_next(
         &mut self,
-        project: Option<&Model<Project>>,
+        provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_next(project, cx),
+                ContextMenu::Completions(menu) => menu.select_next(provider, cx),
                 ContextMenu::CodeActions(menu) => menu.select_next(cx),
             }
             true
@@ -932,12 +934,12 @@ impl ContextMenu {
 
     fn select_last(
         &mut self,
-        project: Option<&Model<Project>>,
+        provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_last(project, cx),
+                ContextMenu::Completions(menu) => menu.select_last(provider, cx),
                 ContextMenu::CodeActions(menu) => menu.select_last(cx),
             }
             true
@@ -991,39 +993,55 @@ struct CompletionsMenu {
 }
 
 impl CompletionsMenu {
-    fn select_first(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
+    fn select_first(
+        &mut self,
+        provider: Option<&dyn CompletionProvider>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = 0;
         self.scroll_handle.scroll_to_item(self.selected_item);
-        self.attempt_resolve_selected_completion_documentation(project, cx);
+        self.attempt_resolve_selected_completion_documentation(provider, cx);
         cx.notify();
     }
 
-    fn select_prev(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
+    fn select_prev(
+        &mut self,
+        provider: Option<&dyn CompletionProvider>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
         } else {
             self.selected_item = self.matches.len() - 1;
         }
         self.scroll_handle.scroll_to_item(self.selected_item);
-        self.attempt_resolve_selected_completion_documentation(project, cx);
+        self.attempt_resolve_selected_completion_documentation(provider, cx);
         cx.notify();
     }
 
-    fn select_next(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
+    fn select_next(
+        &mut self,
+        provider: Option<&dyn CompletionProvider>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item + 1 < self.matches.len() {
             self.selected_item += 1;
         } else {
             self.selected_item = 0;
         }
         self.scroll_handle.scroll_to_item(self.selected_item);
-        self.attempt_resolve_selected_completion_documentation(project, cx);
+        self.attempt_resolve_selected_completion_documentation(provider, cx);
         cx.notify();
     }
 
-    fn select_last(&mut self, project: Option<&Model<Project>>, cx: &mut ViewContext<Editor>) {
+    fn select_last(
+        &mut self,
+        provider: Option<&dyn CompletionProvider>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = self.matches.len() - 1;
         self.scroll_handle.scroll_to_item(self.selected_item);
-        self.attempt_resolve_selected_completion_documentation(project, cx);
+        self.attempt_resolve_selected_completion_documentation(provider, cx);
         cx.notify();
     }
 
@@ -1059,7 +1077,7 @@ impl CompletionsMenu {
 
     fn attempt_resolve_selected_completion_documentation(
         &mut self,
-        project: Option<&Model<Project>>,
+        provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) {
         let settings = EditorSettings::get_global(cx);
@@ -1068,18 +1086,16 @@ impl CompletionsMenu {
         }
 
         let completion_index = self.matches[self.selected_item].candidate_id;
-        let Some(project) = project else {
+        let Some(provider) = provider else {
             return;
         };
 
-        let resolve_task = project.update(cx, |project, cx| {
-            project.resolve_completions(
-                self.buffer.clone(),
-                vec![completion_index],
-                self.completions.clone(),
-                cx,
-            )
-        });
+        let resolve_task = provider.resolve_completions(
+            self.buffer.clone(),
+            vec![completion_index],
+            self.completions.clone(),
+            cx,
+        );
 
         let delay_ms =
             EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
@@ -1671,7 +1687,7 @@ pub(crate) struct NavigationData {
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum GotoDefinitionKind {
+pub enum GotoDefinitionKind {
     Symbol,
     Declaration,
     Type,
@@ -1937,6 +1953,7 @@ impl Editor {
             active_diagnostics: None,
             soft_wrap_mode_override,
             completion_provider: project.clone().map(|project| Box::new(project) as _),
+            semantics_provider: project.clone().map(|project| Rc::new(project) as _),
             collaboration_hub: project.clone().map(|project| Box::new(project) as _),
             project,
             blink_manager: blink_manager.clone(),
@@ -2305,8 +2322,16 @@ impl Editor {
         self.custom_context_menu = Some(Box::new(f))
     }
 
-    pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
-        self.completion_provider = Some(provider);
+    pub fn set_completion_provider(&mut self, provider: Option<Box<dyn CompletionProvider>>) {
+        self.completion_provider = provider;
+    }
+
+    pub fn semantics_provider(&self) -> Option<Rc<dyn SemanticsProvider>> {
+        self.semantics_provider.clone()
+    }
+
+    pub fn set_semantics_provider(&mut self, provider: Option<Rc<dyn SemanticsProvider>>) {
+        self.semantics_provider = provider;
     }
 
     pub fn set_inline_completion_provider<T>(
@@ -4041,7 +4066,7 @@ impl Editor {
     }
 
     fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
-        if self.project.is_none() || self.mode != EditorMode::Full {
+        if self.semantics_provider.is_none() || self.mode != EditorMode::Full {
             return;
         }
 
@@ -4942,6 +4967,11 @@ impl Editor {
         Ok(())
     }
 
+    pub fn clear_code_action_providers(&mut self) {
+        self.code_action_providers.clear();
+        self.available_code_actions.take();
+    }
+
     pub fn push_code_action_provider(
         &mut self,
         provider: Arc<dyn CodeActionProvider>,
@@ -5029,7 +5059,7 @@ impl Editor {
             return None;
         }
 
-        let project = self.project.clone()?;
+        let provider = self.semantics_provider.clone()?;
         let buffer = self.buffer.read(cx);
         let newest_selection = self.selections.newest_anchor().clone();
         let cursor_position = newest_selection.head();
@@ -5045,11 +5075,12 @@ impl Editor {
                 .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
                 .await;
 
-            let highlights = if let Some(highlights) = project
-                .update(&mut cx, |project, cx| {
-                    project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
+            let highlights = if let Some(highlights) = cx
+                .update(|cx| {
+                    provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
                 })
-                .log_err()
+                .ok()
+                .flatten()
             {
                 highlights.await.log_err()
             } else {
@@ -7471,7 +7502,7 @@ impl Editor {
             .context_menu
             .write()
             .as_mut()
-            .map(|menu| menu.select_first(self.project.as_ref(), cx))
+            .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
             .unwrap_or(false)
         {
             return;
@@ -7580,7 +7611,7 @@ impl Editor {
             .context_menu
             .write()
             .as_mut()
-            .map(|menu| menu.select_last(self.project.as_ref(), cx))
+            .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
             .unwrap_or(false)
         {
             return;
@@ -7632,25 +7663,25 @@ impl Editor {
 
     pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.write().as_mut() {
-            context_menu.select_first(self.project.as_ref(), cx);
+            context_menu.select_first(self.completion_provider.as_deref(), cx);
         }
     }
 
     pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.write().as_mut() {
-            context_menu.select_prev(self.project.as_ref(), cx);
+            context_menu.select_prev(self.completion_provider.as_deref(), cx);
         }
     }
 
     pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.write().as_mut() {
-            context_menu.select_next(self.project.as_ref(), cx);
+            context_menu.select_next(self.completion_provider.as_deref(), cx);
         }
     }
 
     pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.write().as_mut() {
-            context_menu.select_last(self.project.as_ref(), cx);
+            context_menu.select_last(self.completion_provider.as_deref(), cx);
         }
     }
 
@@ -9623,7 +9654,7 @@ impl Editor {
         split: bool,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Navigated>> {
-        let Some(workspace) = self.workspace() else {
+        let Some(provider) = self.semantics_provider.clone() else {
             return Task::ready(Ok(Navigated::No));
         };
         let buffer = self.buffer.read(cx);
@@ -9634,13 +9665,9 @@ impl Editor {
             return Task::ready(Ok(Navigated::No));
         };
 
-        let project = workspace.read(cx).project().clone();
-        let definitions = project.update(cx, |project, cx| match kind {
-            GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
-            GotoDefinitionKind::Declaration => project.declaration(&buffer, head, cx),
-            GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
-            GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
-        });
+        let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else {
+            return Task::ready(Ok(Navigated::No));
+        };
 
         cx.spawn(|editor, mut cx| async move {
             let definitions = definitions.await?;
@@ -9697,9 +9724,7 @@ impl Editor {
             return;
         };
 
-        let Some(project) = self.project.clone() else {
-            return;
-        };
+        let project = self.project.clone();
 
         cx.spawn(|_, mut cx| async move {
             let result = find_file(&buffer, project, buffer_position, &mut cx).await;
@@ -10101,7 +10126,7 @@ impl Editor {
     pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
         use language::ToOffset as _;
 
-        let project = self.project.clone()?;
+        let provider = self.semantics_provider.clone()?;
         let selection = self.selections.newest_anchor().clone();
         let (cursor_buffer, cursor_buffer_position) = self
             .buffer
@@ -10118,9 +10143,9 @@ impl Editor {
         let snapshot = cursor_buffer.read(cx).snapshot();
         let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
         let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot);
-        let prepare_rename = project.update(cx, |project, cx| {
-            project.prepare_rename(cursor_buffer.clone(), cursor_buffer_offset, cx)
-        });
+        let prepare_rename = provider
+            .range_for_rename(&cursor_buffer, cursor_buffer_position, cx)
+            .unwrap_or_else(|| Task::ready(Ok(None)));
         drop(snapshot);
 
         Some(cx.spawn(|this, mut cx| async move {
@@ -10291,32 +10316,28 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         let rename = self.take_rename(false, cx)?;
-        let workspace = self.workspace()?;
-        let (start_buffer, start) = self
+        let workspace = self.workspace()?.downgrade();
+        let (buffer, start) = self
             .buffer
             .read(cx)
             .text_anchor_for_position(rename.range.start, cx)?;
-        let (end_buffer, end) = self
+        let (end_buffer, _) = self
             .buffer
             .read(cx)
             .text_anchor_for_position(rename.range.end, cx)?;
-        if start_buffer != end_buffer {
+        if buffer != end_buffer {
             return None;
         }
 
-        let buffer = start_buffer;
-        let range = start..end;
         let old_name = rename.old_name;
         let new_name = rename.editor.read(cx).text(cx);
 
-        let rename = workspace
-            .read(cx)
-            .project()
-            .clone()
-            .update(cx, |project, cx| {
-                project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
-            });
-        let workspace = workspace.downgrade();
+        let rename = self.semantics_provider.as_ref()?.perform_rename(
+            &buffer,
+            start,
+            new_name.clone(),
+            cx,
+        )?;
 
         Some(cx.spawn(|editor, mut cx| async move {
             let project_transaction = rename.await?;
@@ -12371,14 +12392,22 @@ impl Editor {
 
         let mut new_selections_by_buffer = HashMap::default();
         for selection in self.selections.all::<usize>(cx) {
-            for (buffer, mut range, _) in
-                buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+            for (mut buffer_handle, mut range, _) in
+                buffer.range_to_buffer_ranges(selection.range(), cx)
             {
+                // When editing branch buffers, jump to the corresponding location
+                // in their base buffer.
+                let buffer = buffer_handle.read(cx);
+                if let Some(base_buffer) = buffer.diff_base_buffer() {
+                    range = buffer.range_to_version(range, &base_buffer.read(cx).version());
+                    buffer_handle = base_buffer;
+                }
+
                 if selection.reversed {
                     mem::swap(&mut range.start, &mut range.end);
                 }
                 new_selections_by_buffer
-                    .entry(buffer)
+                    .entry(buffer_handle)
                     .or_insert(Vec::new())
                     .push(range)
             }
@@ -12663,24 +12692,13 @@ impl Editor {
     }
 
     pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
-        let Some(project) = self.project.as_ref() else {
+        let Some(provider) = self.semantics_provider.as_ref() else {
             return false;
         };
-        let project = project.read(cx);
 
         let mut supports = false;
         self.buffer().read(cx).for_each_buffer(|buffer| {
-            if !supports {
-                supports = project
-                    .language_servers_for_buffer(buffer.read(cx), cx)
-                    .any(
-                        |(_, server)| match server.capabilities().inlay_hint_provider {
-                            Some(lsp::OneOf::Left(enabled)) => enabled,
-                            Some(lsp::OneOf::Right(_)) => true,
-                            None => false,
-                        },
-                    )
-            }
+            supports |= provider.supports_inlay_hints(buffer, cx);
         });
         supports
     }
@@ -12946,6 +12964,62 @@ impl CollaborationHub for Model<Project> {
     }
 }
 
+pub trait SemanticsProvider {
+    fn hover(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Vec<project::Hover>>>;
+
+    fn inlay_hints(
+        &self,
+        buffer_handle: Model<Buffer>,
+        range: Range<text::Anchor>,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
+
+    fn resolve_inlay_hint(
+        &self,
+        hint: InlayHint,
+        buffer_handle: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<InlayHint>>>;
+
+    fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool;
+
+    fn document_highlights(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Vec<DocumentHighlight>>>>;
+
+    fn definitions(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        kind: GotoDefinitionKind,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Vec<LocationLink>>>>;
+
+    fn range_for_rename(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Option<Range<text::Anchor>>>>>;
+
+    fn perform_rename(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        new_name: String,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<ProjectTransaction>>>;
+}
+
 pub trait CompletionProvider {
     fn completions(
         &self,
@@ -13197,6 +13271,102 @@ impl CompletionProvider for Model<Project> {
     }
 }
 
+impl SemanticsProvider for Model<Project> {
+    fn hover(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Vec<project::Hover>>> {
+        Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
+    }
+
+    fn document_highlights(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Vec<DocumentHighlight>>>> {
+        Some(self.update(cx, |project, cx| {
+            project.document_highlights(buffer, position, cx)
+        }))
+    }
+
+    fn definitions(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        kind: GotoDefinitionKind,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Vec<LocationLink>>>> {
+        Some(self.update(cx, |project, cx| match kind {
+            GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx),
+            GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx),
+            GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx),
+            GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx),
+        }))
+    }
+
+    fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
+        // TODO: make this work for remote projects
+        self.read(cx)
+            .language_servers_for_buffer(buffer.read(cx), cx)
+            .any(
+                |(_, server)| match server.capabilities().inlay_hint_provider {
+                    Some(lsp::OneOf::Left(enabled)) => enabled,
+                    Some(lsp::OneOf::Right(_)) => true,
+                    None => false,
+                },
+            )
+    }
+
+    fn inlay_hints(
+        &self,
+        buffer_handle: Model<Buffer>,
+        range: Range<text::Anchor>,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
+        Some(self.update(cx, |project, cx| {
+            project.inlay_hints(buffer_handle, range, cx)
+        }))
+    }
+
+    fn resolve_inlay_hint(
+        &self,
+        hint: InlayHint,
+        buffer_handle: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<InlayHint>>> {
+        Some(self.update(cx, |project, cx| {
+            project.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
+        }))
+    }
+
+    fn range_for_rename(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
+        Some(self.update(cx, |project, cx| {
+            project.prepare_rename(buffer.clone(), position, cx)
+        }))
+    }
+
+    fn perform_rename(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        new_name: String,
+        cx: &mut AppContext,
+    ) -> Option<Task<Result<ProjectTransaction>>> {
+        Some(self.update(cx, |project, cx| {
+            project.perform_rename(buffer.clone(), position, new_name, cx)
+        }))
+    }
+}
+
 fn inlay_hint_settings(
     location: Anchor,
     snapshot: &MultiBufferSnapshot,

crates/editor/src/hover_links.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     hover_popover::{self, InlayHover},
     scroll::ScrollAmount,
-    Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
-    Navigated, PointForPosition, SelectPhase,
+    Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+    GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
 };
 use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
 use language::{Bias, ToOffset};
@@ -14,12 +14,12 @@ use project::{
 };
 use std::ops::Range;
 use theme::ActiveTheme as _;
-use util::{maybe, ResultExt, TryFutureExt};
+use util::{maybe, ResultExt, TryFutureExt as _};
 
 #[derive(Debug)]
 pub struct HoveredLinkState {
     pub last_trigger_point: TriggerPoint,
-    pub preferred_kind: LinkDefinitionKind,
+    pub preferred_kind: GotoDefinitionKind,
     pub symbol_range: Option<RangeInEditor>,
     pub links: Vec<HoverLink>,
     pub task: Option<Task<Option<()>>>,
@@ -428,12 +428,6 @@ pub fn update_inlay_link_and_hover_points(
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum LinkDefinitionKind {
-    Symbol,
-    Type,
-}
-
 pub fn show_link_definition(
     shift_held: bool,
     editor: &mut Editor,
@@ -442,8 +436,8 @@ pub fn show_link_definition(
     cx: &mut ViewContext<Editor>,
 ) {
     let preferred_kind = match trigger_point {
-        TriggerPoint::Text(_) if !shift_held => LinkDefinitionKind::Symbol,
-        _ => LinkDefinitionKind::Type,
+        TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,
+        _ => GotoDefinitionKind::Type,
     };
 
     let (mut hovered_link_state, is_cached) =
@@ -505,6 +499,7 @@ pub fn show_link_definition(
         editor.hide_hovered_link(cx)
     }
     let project = editor.project.clone();
+    let provider = editor.semantics_provider.clone();
 
     let snapshot = snapshot.buffer_snapshot.clone();
     hovered_link_state.task = Some(cx.spawn(|this, mut cx| {
@@ -522,54 +517,40 @@ pub fn show_link_definition(
                             (range, vec![HoverLink::Url(url)])
                         })
                         .ok()
-                    } else if let Some(project) = project {
-                        if let Some((filename_range, filename)) =
-                            find_file(&buffer, project.clone(), buffer_position, &mut cx).await
-                        {
-                            let range = maybe!({
-                                let start =
-                                    snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
-                                let end =
-                                    snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
-                                Some(RangeInEditor::Text(start..end))
-                            });
-
-                            Some((range, vec![HoverLink::File(filename)]))
+                    } else if let Some((filename_range, filename)) =
+                        find_file(&buffer, project.clone(), buffer_position, &mut cx).await
+                    {
+                        let range = maybe!({
+                            let start =
+                                snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
+                            let end = snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
+                            Some(RangeInEditor::Text(start..end))
+                        });
+
+                        Some((range, vec![HoverLink::File(filename)]))
+                    } else if let Some(provider) = provider {
+                        let task = cx.update(|cx| {
+                            provider.definitions(&buffer, buffer_position, preferred_kind, cx)
+                        })?;
+                        if let Some(task) = task {
+                            task.await.ok().map(|definition_result| {
+                                (
+                                    definition_result.iter().find_map(|link| {
+                                        link.origin.as_ref().and_then(|origin| {
+                                            let start = snapshot.anchor_in_excerpt(
+                                                excerpt_id,
+                                                origin.range.start,
+                                            )?;
+                                            let end = snapshot
+                                                .anchor_in_excerpt(excerpt_id, origin.range.end)?;
+                                            Some(RangeInEditor::Text(start..end))
+                                        })
+                                    }),
+                                    definition_result.into_iter().map(HoverLink::Text).collect(),
+                                )
+                            })
                         } else {
-                            // query the LSP for definition info
-                            project
-                                .update(&mut cx, |project, cx| match preferred_kind {
-                                    LinkDefinitionKind::Symbol => {
-                                        project.definition(&buffer, buffer_position, cx)
-                                    }
-
-                                    LinkDefinitionKind::Type => {
-                                        project.type_definition(&buffer, buffer_position, cx)
-                                    }
-                                })?
-                                .await
-                                .ok()
-                                .map(|definition_result| {
-                                    (
-                                        definition_result.iter().find_map(|link| {
-                                            link.origin.as_ref().and_then(|origin| {
-                                                let start = snapshot.anchor_in_excerpt(
-                                                    excerpt_id,
-                                                    origin.range.start,
-                                                )?;
-                                                let end = snapshot.anchor_in_excerpt(
-                                                    excerpt_id,
-                                                    origin.range.end,
-                                                )?;
-                                                Some(RangeInEditor::Text(start..end))
-                                            })
-                                        }),
-                                        definition_result
-                                            .into_iter()
-                                            .map(HoverLink::Text)
-                                            .collect(),
-                                    )
-                                })
+                            None
                         }
                     } else {
                         None
@@ -708,10 +689,11 @@ pub(crate) fn find_url(
 
 pub(crate) async fn find_file(
     buffer: &Model<language::Buffer>,
-    project: Model<Project>,
+    project: Option<Model<Project>>,
     position: text::Anchor,
     cx: &mut AsyncWindowContext,
 ) -> Option<(Range<text::Anchor>, ResolvedPath)> {
+    let project = project?;
     let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
     let scope = snapshot.language_scope_at(position);
     let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;

crates/editor/src/hover_popover.rs 🔗

@@ -195,32 +195,22 @@ fn show_hover(
     anchor: Anchor,
     ignore_timeout: bool,
     cx: &mut ViewContext<Editor>,
-) {
+) -> Option<()> {
     if editor.pending_rename.is_some() {
-        return;
+        return None;
     }
 
     let snapshot = editor.snapshot(cx);
 
-    let (buffer, buffer_position) =
-        if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) {
-            output
-        } else {
-            return;
-        };
+    let (buffer, buffer_position) = editor
+        .buffer
+        .read(cx)
+        .text_anchor_for_position(anchor, cx)?;
 
-    let excerpt_id =
-        if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) {
-            excerpt_id
-        } else {
-            return;
-        };
+    let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
 
-    let project = if let Some(project) = editor.project.clone() {
-        project
-    } else {
-        return;
-    };
+    let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
+    let provider = editor.semantics_provider.clone()?;
 
     if !ignore_timeout {
         if same_info_hover(editor, &snapshot, anchor)
@@ -228,7 +218,7 @@ fn show_hover(
             || editor.hover_state.diagnostic_popover.is_some()
         {
             // Hover triggered from same location as last time. Don't show again.
-            return;
+            return None;
         } else {
             hide_hover(editor, cx);
         }
@@ -240,7 +230,7 @@ fn show_hover(
             .cmp(&anchor, &snapshot.buffer_snapshot)
             .is_eq()
         {
-            return;
+            return None;
         }
     }
 
@@ -262,12 +252,7 @@ fn show_hover(
                 total_delay
             };
 
-            // query the LSP for hover info
-            let hover_request = cx.update(|cx| {
-                project.update(cx, |project, cx| {
-                    project.hover(&buffer, buffer_position, cx)
-                })
-            })?;
+            let hover_request = cx.update(|cx| provider.hover(&buffer, buffer_position, cx))?;
 
             if let Some(delay) = delay {
                 delay.await;
@@ -377,8 +362,11 @@ fn show_hover(
                 this.hover_state.diagnostic_popover = diagnostic_popover;
             })?;
 
-            let hovers_response = hover_request.await;
-            let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
+            let hovers_response = if let Some(hover_request) = hover_request {
+                hover_request.await
+            } else {
+                Vec::new()
+            };
             let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
             let mut hover_highlights = Vec::with_capacity(hovers_response.len());
             let mut info_popovers = Vec::with_capacity(hovers_response.len());
@@ -451,6 +439,7 @@ fn show_hover(
     });
 
     editor.hover_state.info_task = Some(task);
+    None
 }
 
 fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool {

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -591,21 +591,13 @@ impl InlayHintCache {
                     drop(guard);
                     cx.spawn(|editor, mut cx| async move {
                         let resolved_hint_task = editor.update(&mut cx, |editor, cx| {
-                            editor
-                                .buffer()
-                                .read(cx)
-                                .buffer(buffer_id)
-                                .and_then(|buffer| {
-                                    let project = editor.project.as_ref()?;
-                                    Some(project.update(cx, |project, cx| {
-                                        project.resolve_inlay_hint(
-                                            hint_to_resolve,
-                                            buffer,
-                                            server_id,
-                                            cx,
-                                        )
-                                    }))
-                                })
+                            let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
+                            editor.semantics_provider.as_ref()?.resolve_inlay_hint(
+                                hint_to_resolve,
+                                buffer,
+                                server_id,
+                                cx,
+                            )
                         })?;
                         if let Some(resolved_hint_task) = resolved_hint_task {
                             let mut resolved_hint =
@@ -895,11 +887,13 @@ fn fetch_and_update_hints(
 ) -> Task<anyhow::Result<()>> {
     cx.spawn(|editor, mut cx| async move {
         let buffer_snapshot = excerpt_buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
-        let (lsp_request_limiter, multi_buffer_snapshot) = editor.update(&mut cx, |editor, cx| {
-            let multi_buffer_snapshot  = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
-            (lsp_request_limiter, multi_buffer_snapshot)
-        })?;
+        let (lsp_request_limiter, multi_buffer_snapshot) =
+            editor.update(&mut cx, |editor, cx| {
+                let multi_buffer_snapshot =
+                    editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+                let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
+                (lsp_request_limiter, multi_buffer_snapshot)
+            })?;
 
         let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
             (None, false)
@@ -909,12 +903,15 @@ fn fetch_and_update_hints(
                 None => (Some(lsp_request_limiter.acquire().await), true),
             }
         };
-        let fetch_range_to_log =
-            fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
+        let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot)
+            ..fetch_range.end.to_point(&buffer_snapshot);
         let inlay_hints_fetch_task = editor
             .update(&mut cx, |editor, cx| {
                 if got_throttled {
-                    let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
+                    let query_not_around_visible_range = match editor
+                        .excerpts_for_inlay_hints_query(None, cx)
+                        .remove(&query.excerpt_id)
+                    {
                         Some((_, _, current_visible_range)) => {
                             let visible_offset_length = current_visible_range.len();
                             let double_visible_range = current_visible_range
@@ -928,11 +925,11 @@ fn fetch_and_update_hints(
                                 .contains(&fetch_range.start.to_offset(&buffer_snapshot))
                                 && !double_visible_range
                                     .contains(&fetch_range.end.to_offset(&buffer_snapshot))
-                        },
+                        }
                         None => true,
                     };
                     if query_not_around_visible_range {
-                        log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
+                        // log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
                         if let Some(task_ranges) = editor
                             .inlay_hint_cache
                             .update_tasks
@@ -943,16 +940,12 @@ fn fetch_and_update_hints(
                         return None;
                     }
                 }
+
+                let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
                 editor
-                    .buffer()
-                    .read(cx)
-                    .buffer(query.buffer_id)
-                    .and_then(|buffer| {
-                        let project = editor.project.as_ref()?;
-                        Some(project.update(cx, |project, cx| {
-                            project.inlay_hints(buffer, fetch_range.clone(), cx)
-                        }))
-                    })
+                    .semantics_provider
+                    .as_ref()?
+                    .inlay_hints(buffer, fetch_range.clone(), cx)
             })
             .ok()
             .flatten();
@@ -1004,12 +997,12 @@ fn fetch_and_update_hints(
             })
             .await;
         if let Some(new_update) = new_update {
-            log::debug!(
-                "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
-                new_update.remove_from_visible.len(),
-                new_update.remove_from_cache.len(),
-                new_update.add_to_cache.len()
-            );
+            // log::debug!(
+            //     "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
+            //     new_update.remove_from_visible.len(),
+            //     new_update.remove_from_cache.len(),
+            //     new_update.add_to_cache.len()
+            // );
             log::trace!("New update: {new_update:?}");
             editor
                 .update(&mut cx, |editor, cx| {

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Editor, EditorEvent};
+use crate::{Editor, EditorEvent, SemanticsProvider};
 use collections::HashSet;
 use futures::{channel::mpsc, future::join_all};
 use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
@@ -6,7 +6,7 @@ use language::{Buffer, BufferEvent, Capability};
 use multi_buffer::{ExcerptRange, MultiBuffer};
 use project::Project;
 use smol::stream::StreamExt;
-use std::{any::TypeId, ops::Range, time::Duration};
+use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
 use text::ToOffset;
 use ui::prelude::*;
 use workspace::{
@@ -35,6 +35,12 @@ struct RecalculateDiff {
     debounce: bool,
 }
 
+/// A provider of code semantics for branch buffers.
+///
+/// Requests in edited regions will return nothing, but requests in unchanged
+/// regions will be translated into the base buffer's coordinates.
+struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
+
 impl ProposedChangesEditor {
     pub fn new<T: ToOffset>(
         buffers: Vec<ProposedChangesBuffer<T>>,
@@ -66,6 +72,13 @@ impl ProposedChangesEditor {
             editor: cx.new_view(|cx| {
                 let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
                 editor.set_expand_all_diff_hunks();
+                editor.set_completion_provider(None);
+                editor.clear_code_action_providers();
+                editor.set_semantics_provider(
+                    editor
+                        .semantics_provider()
+                        .map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
+                );
                 editor
             }),
             recalculate_diffs_tx,
@@ -76,7 +89,7 @@ impl ProposedChangesEditor {
 
                     while recalculate_diff.debounce {
                         cx.background_executor()
-                            .timer(Duration::from_millis(250))
+                            .timer(Duration::from_millis(50))
                             .await;
                         let mut had_further_changes = false;
                         while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
@@ -245,3 +258,103 @@ impl ToolbarItemView for ProposedChangesEditorToolbar {
         self.get_toolbar_item_location()
     }
 }
+
+impl BranchBufferSemanticsProvider {
+    fn to_base(
+        &self,
+        buffer: &Model<Buffer>,
+        positions: &[text::Anchor],
+        cx: &AppContext,
+    ) -> Option<Model<Buffer>> {
+        let base_buffer = buffer.read(cx).diff_base_buffer()?;
+        let version = base_buffer.read(cx).version();
+        if positions
+            .iter()
+            .any(|position| !version.observed(position.timestamp))
+        {
+            return None;
+        }
+        Some(base_buffer)
+    }
+}
+
+impl SemanticsProvider for BranchBufferSemanticsProvider {
+    fn hover(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<Vec<project::Hover>>> {
+        let buffer = self.to_base(buffer, &[position], cx)?;
+        self.0.hover(&buffer, position, cx)
+    }
+
+    fn inlay_hints(
+        &self,
+        buffer: Model<Buffer>,
+        range: Range<text::Anchor>,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
+        let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
+        self.0.inlay_hints(buffer, range, cx)
+    }
+
+    fn resolve_inlay_hint(
+        &self,
+        hint: project::InlayHint,
+        buffer: Model<Buffer>,
+        server_id: lsp::LanguageServerId,
+        cx: &mut AppContext,
+    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
+        let buffer = self.to_base(&buffer, &[], cx)?;
+        self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
+    }
+
+    fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
+        if let Some(buffer) = self.to_base(&buffer, &[], cx) {
+            self.0.supports_inlay_hints(&buffer, cx)
+        } else {
+            false
+        }
+    }
+
+    fn document_highlights(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        cx: &mut AppContext,
+    ) -> Option<Task<gpui::Result<Vec<project::DocumentHighlight>>>> {
+        let buffer = self.to_base(&buffer, &[position], cx)?;
+        self.0.document_highlights(&buffer, position, cx)
+    }
+
+    fn definitions(
+        &self,
+        buffer: &Model<Buffer>,
+        position: text::Anchor,
+        kind: crate::GotoDefinitionKind,
+        cx: &mut AppContext,
+    ) -> Option<Task<gpui::Result<Vec<project::LocationLink>>>> {
+        let buffer = self.to_base(&buffer, &[position], cx)?;
+        self.0.definitions(&buffer, position, kind, cx)
+    }
+
+    fn range_for_rename(
+        &self,
+        _: &Model<Buffer>,
+        _: text::Anchor,
+        _: &mut AppContext,
+    ) -> Option<Task<gpui::Result<Option<Range<text::Anchor>>>>> {
+        None
+    }
+
+    fn perform_rename(
+        &self,
+        _: &Model<Buffer>,
+        _: text::Anchor,
+        _: String,
+        _: &mut AppContext,
+    ) -> Option<Task<gpui::Result<project::ProjectTransaction>>> {
+        None
+    }
+}

crates/language/src/buffer_tests.rs 🔗

@@ -2381,20 +2381,14 @@ async fn test_find_matching_indent(cx: &mut TestAppContext) {
 fn test_branch_and_merge(cx: &mut TestAppContext) {
     cx.update(|cx| init_settings(cx, |_| {}));
 
-    let base_buffer = cx.new_model(|cx| Buffer::local("one\ntwo\nthree\n", cx));
+    let base = cx.new_model(|cx| Buffer::local("one\ntwo\nthree\n", cx));
 
     // Create a remote replica of the base buffer.
-    let base_buffer_replica = cx.new_model(|cx| {
-        Buffer::from_proto(
-            1,
-            Capability::ReadWrite,
-            base_buffer.read(cx).to_proto(cx),
-            None,
-        )
-        .unwrap()
+    let base_replica = cx.new_model(|cx| {
+        Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
     });
-    base_buffer.update(cx, |_buffer, cx| {
-        cx.subscribe(&base_buffer_replica, |this, _, event, cx| {
+    base.update(cx, |_buffer, cx| {
+        cx.subscribe(&base_replica, |this, _, event, cx| {
             if let BufferEvent::Operation {
                 operation,
                 is_local: true,
@@ -2407,14 +2401,14 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
     });
 
     // Create a branch, which initially has the same state as the base buffer.
-    let branch_buffer = base_buffer.update(cx, |buffer, cx| buffer.branch(cx));
-    branch_buffer.read_with(cx, |buffer, _| {
+    let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
+    branch.read_with(cx, |buffer, _| {
         assert_eq!(buffer.text(), "one\ntwo\nthree\n");
     });
 
     // Edits to the branch are not applied to the base.
-    branch_buffer.update(cx, |branch_buffer, cx| {
-        branch_buffer.edit(
+    branch.update(cx, |buffer, cx| {
+        buffer.edit(
             [
                 (Point::new(1, 0)..Point::new(1, 0), "1.5\n"),
                 (Point::new(2, 0)..Point::new(2, 5), "THREE"),
@@ -2423,64 +2417,74 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
             cx,
         )
     });
-    branch_buffer.read_with(cx, |branch_buffer, cx| {
-        assert_eq!(base_buffer.read(cx).text(), "one\ntwo\nthree\n");
-        assert_eq!(branch_buffer.text(), "one\n1.5\ntwo\nTHREE\n");
+    branch.read_with(cx, |buffer, cx| {
+        assert_eq!(base.read(cx).text(), "one\ntwo\nthree\n");
+        assert_eq!(buffer.text(), "one\n1.5\ntwo\nTHREE\n");
+    });
+
+    // Convert from branch buffer ranges to the corresoponing ranges in the
+    // base buffer.
+    branch.read_with(cx, |buffer, cx| {
+        assert_eq!(
+            buffer.range_to_version(4..7, &base.read(cx).version()),
+            4..4
+        );
+        assert_eq!(
+            buffer.range_to_version(2..9, &base.read(cx).version()),
+            2..5
+        );
     });
 
     // The branch buffer maintains a diff with respect to its base buffer.
-    start_recalculating_diff(&branch_buffer, cx);
+    start_recalculating_diff(&branch, cx);
     cx.run_until_parked();
     assert_diff_hunks(
-        &branch_buffer,
+        &branch,
         cx,
         &[(1..2, "", "1.5\n"), (3..4, "three\n", "THREE\n")],
     );
 
     // Edits to the base are applied to the branch.
-    base_buffer.update(cx, |buffer, cx| {
+    base.update(cx, |buffer, cx| {
         buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx)
     });
-    branch_buffer.read_with(cx, |branch_buffer, cx| {
-        assert_eq!(base_buffer.read(cx).text(), "ZERO\none\ntwo\nthree\n");
-        assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n");
+    branch.read_with(cx, |buffer, cx| {
+        assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\nthree\n");
+        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n");
     });
 
     // Until the git diff recalculation is complete, the git diff references
     // the previous content of the base buffer, so that it stays in sync.
-    start_recalculating_diff(&branch_buffer, cx);
+    start_recalculating_diff(&branch, cx);
     assert_diff_hunks(
-        &branch_buffer,
+        &branch,
         cx,
         &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")],
     );
     cx.run_until_parked();
     assert_diff_hunks(
-        &branch_buffer,
+        &branch,
         cx,
         &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")],
     );
 
     // Edits to any replica of the base are applied to the branch.
-    base_buffer_replica.update(cx, |buffer, cx| {
+    base_replica.update(cx, |buffer, cx| {
         buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "2.5\n")], None, cx)
     });
-    branch_buffer.read_with(cx, |branch_buffer, cx| {
-        assert_eq!(base_buffer.read(cx).text(), "ZERO\none\ntwo\n2.5\nthree\n");
-        assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
+    branch.read_with(cx, |buffer, cx| {
+        assert_eq!(base.read(cx).text(), "ZERO\none\ntwo\n2.5\nthree\n");
+        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
     });
 
     // Merging the branch applies all of its changes to the base.
-    branch_buffer.update(cx, |branch_buffer, cx| {
-        branch_buffer.merge_into_base(Vec::new(), cx);
+    branch.update(cx, |buffer, cx| {
+        buffer.merge_into_base(Vec::new(), cx);
     });
 
-    branch_buffer.update(cx, |branch_buffer, cx| {
-        assert_eq!(
-            base_buffer.read(cx).text(),
-            "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n"
-        );
-        assert_eq!(branch_buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
+    branch.update(cx, |buffer, cx| {
+        assert_eq!(base.read(cx).text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
+        assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\n2.5\nTHREE\n");
     });
 }
 

crates/project/src/project.rs 🔗

@@ -2724,16 +2724,16 @@ impl Project {
             cx,
         )
     }
+
     pub fn perform_rename<T: ToPointUtf16>(
         &mut self,
         buffer: Model<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.perform_rename_impl(buffer, position, new_name, push_to_history, cx)
+        self.perform_rename_impl(buffer, position, new_name, true, cx)
     }
 
     pub fn on_type_format<T: ToPointUtf16>(

crates/project/src/project_tests.rs 🔗

@@ -3892,7 +3892,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
     assert_eq!(range, 6..9);
 
     let response = project.update(cx, |project, cx| {
-        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
+        project.perform_rename(buffer.clone(), 7, "THREE".to_string(), cx)
     });
     fake_server
         .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -434,7 +434,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
 
     project
         .update(cx, |project, cx| {
-            project.perform_rename(buffer.clone(), 3, "two".to_string(), true, cx)
+            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
         })
         .await
         .unwrap();

crates/text/src/text.rs 🔗

@@ -2439,6 +2439,42 @@ impl BufferSnapshot {
         }
         false
     }
+
+    pub fn range_to_version(&self, range: Range<usize>, version: &clock::Global) -> Range<usize> {
+        let mut offsets = self.offsets_to_version([range.start, range.end], version);
+        offsets.next().unwrap()..offsets.next().unwrap()
+    }
+
+    /// Converts the given sequence of offsets into their corresponding offsets
+    /// at a prior version of this buffer.
+    pub fn offsets_to_version<'a>(
+        &'a self,
+        offsets: impl 'a + IntoIterator<Item = usize>,
+        version: &'a clock::Global,
+    ) -> impl 'a + Iterator<Item = usize> {
+        let mut edits = self.edits_since(version).peekable();
+        let mut last_old_end = 0;
+        let mut last_new_end = 0;
+        offsets.into_iter().map(move |new_offset| {
+            while let Some(edit) = edits.peek() {
+                if edit.new.start > new_offset {
+                    break;
+                }
+
+                if edit.new.end <= new_offset {
+                    last_new_end = edit.new.end;
+                    last_old_end = edit.old.end;
+                    edits.next();
+                    continue;
+                }
+
+                let overshoot = new_offset - edit.new.start;
+                return (edit.old.start + overshoot).min(edit.old.end);
+            }
+
+            last_old_end + new_offset.saturating_sub(last_new_end)
+        })
+    }
 }
 
 struct RopeBuilder<'a> {