From 1c613e6c32e3d2efbdd4381dd8f5af2a8fd61cb8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Oct 2024 16:55:25 -0700 Subject: [PATCH] Fix some issues with branch buffers (#18945) * `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 --- crates/assistant/src/assistant_panel.rs | 2 +- crates/assistant/src/prompt_library.rs | 4 +- .../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 +- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/text/src/text.rs | 36 ++ 13 files changed, 555 insertions(+), 268 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index c3b4fcc4dc9218..e4d02e5bb733a7 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/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 }); diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 6c43579d6ebd02..298bb322265c5f 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/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); } diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 38aaf2faa3b329..7023a8d07ed76c 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 15b833c1dcbba5..5b5401af178fb7 100644 --- a/crates/editor/src/editor.rs +++ b/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, soft_wrap_mode_override: Option, project: Option>, + semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, blink_manager: Model, @@ -884,12 +886,12 @@ enum ContextMenu { impl ContextMenu { fn select_first( &mut self, - project: Option<&Model>, + provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> 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>, + provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> 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>, + provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> 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>, + provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> 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>, cx: &mut ViewContext) { + fn select_first( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { 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>, cx: &mut ViewContext) { + fn select_prev( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { 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>, cx: &mut ViewContext) { + fn select_next( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { 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>, cx: &mut ViewContext) { + fn select_last( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { 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>, + provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { 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) { - self.completion_provider = Some(provider); + pub fn set_completion_provider(&mut self, provider: Option>) { + self.completion_provider = provider; + } + + pub fn semantics_provider(&self) -> Option> { + self.semantics_provider.clone() + } + + pub fn set_semantics_provider(&mut self, provider: Option>) { + self.semantics_provider = provider; } pub fn set_inline_completion_provider( @@ -4041,7 +4066,7 @@ impl Editor { } fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext) { - 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, @@ -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) { 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) { 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) { 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) { 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, ) -> Task> { - 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) -> Option>> { 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, ) -> Option>> { 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::(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 { } } +pub trait SemanticsProvider { + fn hover( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>; + + fn inlay_hints( + &self, + buffer_handle: Model, + range: Range, + cx: &mut AppContext, + ) -> Option>>>; + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Model, + server_id: LanguageServerId, + cx: &mut AppContext, + ) -> Option>>; + + fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool; + + fn document_highlights( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>>; + + fn definitions( + &self, + buffer: &Model, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut AppContext, + ) -> Option>>>; + + fn range_for_rename( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>>>; + + fn perform_rename( + &self, + buffer: &Model, + position: text::Anchor, + new_name: String, + cx: &mut AppContext, + ) -> Option>>; +} + pub trait CompletionProvider { fn completions( &self, @@ -13197,6 +13271,102 @@ impl CompletionProvider for Model { } } +impl SemanticsProvider for Model { + fn hover( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>> { + Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) + } + + fn document_highlights( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.document_highlights(buffer, position, cx) + })) + } + + fn definitions( + &self, + buffer: &Model, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut AppContext, + ) -> Option>>> { + 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, 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, + range: Range, + cx: &mut AppContext, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.inlay_hints(buffer_handle, range, cx) + })) + } + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Model, + server_id: LanguageServerId, + cx: &mut AppContext, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) + })) + } + + fn range_for_rename( + &self, + buffer: &Model, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>>> { + Some(self.update(cx, |project, cx| { + project.prepare_rename(buffer.clone(), position, cx) + })) + } + + fn perform_rename( + &self, + buffer: &Model, + position: text::Anchor, + new_name: String, + cx: &mut AppContext, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.perform_rename(buffer.clone(), position, new_name, cx) + })) + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index ac30b9199601ae..4a636f673abb7d 100644 --- a/crates/editor/src/hover_links.rs +++ b/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, pub links: Vec, pub 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, ) { 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, - project: Model, + project: Option>, position: text::Anchor, cx: &mut AsyncWindowContext, ) -> Option<(Range, 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)?; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e20b6a3dc85d73..da232431880b37 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -195,32 +195,22 @@ fn show_hover( anchor: Anchor, ignore_timeout: bool, cx: &mut ViewContext, -) { +) -> 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 { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index ca2db70a70c2ad..877f02eefe203d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/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> { 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| { diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 8c8aa710a2f72c..056daf28a839e9 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/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); + impl ProposedChangesEditor { pub fn new( buffers: Vec>, @@ -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, + positions: &[text::Anchor], + cx: &AppContext, + ) -> Option> { + 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, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>> { + let buffer = self.to_base(buffer, &[position], cx)?; + self.0.hover(&buffer, position, cx) + } + + fn inlay_hints( + &self, + buffer: Model, + range: Range, + cx: &mut AppContext, + ) -> Option>>> { + 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, + server_id: lsp::LanguageServerId, + cx: &mut AppContext, + ) -> Option>> { + let buffer = self.to_base(&buffer, &[], cx)?; + self.0.resolve_inlay_hint(hint, buffer, server_id, cx) + } + + fn supports_inlay_hints(&self, buffer: &Model, 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, + position: text::Anchor, + cx: &mut AppContext, + ) -> Option>>> { + let buffer = self.to_base(&buffer, &[position], cx)?; + self.0.document_highlights(&buffer, position, cx) + } + + fn definitions( + &self, + buffer: &Model, + position: text::Anchor, + kind: crate::GotoDefinitionKind, + cx: &mut AppContext, + ) -> Option>>> { + let buffer = self.to_base(&buffer, &[position], cx)?; + self.0.definitions(&buffer, position, kind, cx) + } + + fn range_for_rename( + &self, + _: &Model, + _: text::Anchor, + _: &mut AppContext, + ) -> Option>>>> { + None + } + + fn perform_rename( + &self, + _: &Model, + _: text::Anchor, + _: String, + _: &mut AppContext, + ) -> Option>> { + None + } +} diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 83c35cbeca56bd..be6fb4f631f2d1 100644 --- a/crates/language/src/buffer_tests.rs +++ b/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"); }); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 214436231dd82a..7f02d3f6dd76e6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2724,16 +2724,16 @@ impl Project { cx, ) } + pub fn perform_rename( &mut self, buffer: Model, position: T, new_name: String, - push_to_history: bool, cx: &mut ModelContext, ) -> Task> { 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( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ee448543be9701..f5cfba2ffdbacc 100644 --- a/crates/project/src/project_tests.rs +++ b/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::(|params, _| async move { diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 6e962a134a3f16..10cfb517abb802 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/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(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 6c941401be37cb..b1e6be8a1cb74f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2439,6 +2439,42 @@ impl BufferSnapshot { } false } + + pub fn range_to_version(&self, range: Range, version: &clock::Global) -> Range { + 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, + version: &'a clock::Global, + ) -> impl 'a + Iterator { + 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> {