Detailed changes
@@ -73,6 +73,16 @@
// Whether to show git diff indicators in the scrollbar.
"git_diff": true
},
+ // Inlay hint related settings
+ "inlay_hints": {
+ // Global switch to toggle hints on and off, switched off by default.
+ "enabled": false,
+ // Toggle certain types of hints on and off, all switched on by default.
+ "show_type_hints": true,
+ "show_parameter_hints": true,
+ // Corresponds to null/None LSP hint type value.
+ "show_other_hints": true
+ },
"project_panel": {
// Whether to show the git status in the project panel.
"git_status": true,
@@ -201,6 +201,7 @@ impl Server {
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
+ .add_message_handler(refresh_inlay_hints)
.add_request_handler(forward_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -226,6 +227,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
+ .add_request_handler(forward_project_request::<proto::InlayHints>)
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(update_buffer_file)
@@ -1574,6 +1576,10 @@ async fn update_worktree_settings(
Ok(())
}
+async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
+ broadcast_project_message(request.project_id, request, session).await
+}
+
async fn start_language_server(
request: proto::StartLanguageServer,
session: Session,
@@ -1750,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
}
async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
- let project_id = ProjectId::from_proto(request.project_id);
+ broadcast_project_message(request.project_id, request, session).await
+}
+
+async fn broadcast_project_message<T: EnvelopedMessage>(
+ project_id: u64,
+ request: T,
+ session: Session,
+) -> Result<()> {
+ let project_id = ProjectId::from_proto(project_id);
let project_connection_ids = session
.db()
.await
@@ -18,7 +18,7 @@ use gpui::{
};
use indoc::indoc;
use language::{
- language_settings::{AllLanguageSettings, Formatter},
+ language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
};
@@ -34,7 +34,7 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
sync::{
- atomic::{AtomicBool, Ordering::SeqCst},
+ atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
Arc,
},
};
@@ -7800,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host(
});
}
+#[gpui::test]
+async fn test_mutual_editor_inlay_hint_cache_update(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry.add(Arc::clone(&language));
+ client_b.language_registry.add(language);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ cx_a.foreground().start_waiting();
+
+ let _buffer_a = project_a
+ .update(cx_a, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let next_call_id = Arc::new(AtomicU32::new(0));
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_next_call_id = Arc::clone(&next_call_id);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+ let mut new_hints = Vec::with_capacity(current_call_id as usize);
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if current_call_id == 0 {
+ break;
+ }
+ current_call_id -= 1;
+ }
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+
+ cx_a.foreground().finish_waiting();
+ cx_a.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string()],
+ extract_hint_labels(editor),
+ "Host should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Host editor update the cache version after every cache/view change",
+ );
+ });
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor_b.update(cx_b, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+ editor.handle_input(":", cx);
+ cx.focus(&editor_b);
+ edits_made += 1;
+ });
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string(), "2".to_string()],
+ extract_hint_labels(editor),
+ "Host should get hints from the 1st edit and 1st LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string()
+ ],
+ extract_hint_labels(editor),
+ "Guest should get hints the 1st edit and 2nd LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+
+ editor_a.update(cx_a, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("a change to increment both buffers' versions", cx);
+ cx.focus(&editor_a);
+ edits_made += 1;
+ });
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string()
+ ],
+ extract_hint_labels(editor),
+ "Host should get hints from 3rd edit, 5th LSP query: \
+4th query was made by guest (but not applied) due to cache invalidation logic"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Guest should get hints from 3rd edit, 6th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx_a.foreground().run_until_parked();
+ cx_b.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ "6".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Host should react to /refresh LSP request and get new hints from 7th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Host should accepted all edits and bump its cache version every time"
+ );
+ });
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec![
+ "0".to_string(),
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ "5".to_string(),
+ "6".to_string(),
+ "7".to_string(),
+ ],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version,
+ edits_made,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_inlay_hint_refresh_is_forwarded(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ cx.update_global(|store: &mut SettingsStore, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: false,
+ show_other_hints: true,
+ })
+ });
+ });
+ });
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ client_a.language_registry.add(Arc::clone(&language));
+ client_b.language_registry.add(language);
+
+ client_a
+ .fs
+ .insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let workspace_a = client_a.build_workspace(&project_a, cx_a);
+ let workspace_b = client_b.build_workspace(&project_b, cx_b);
+ cx_a.foreground().start_waiting();
+ cx_b.foreground().start_waiting();
+
+ let editor_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let editor_b = workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ let next_call_id = Arc::new(AtomicU32::new(0));
+ fake_language_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_next_call_id = Arc::clone(&next_call_id);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+ let mut new_hints = Vec::with_capacity(current_call_id as usize);
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if current_call_id == 0 {
+ break;
+ }
+ current_call_id -= 1;
+ }
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await
+ .unwrap();
+ cx_a.foreground().finish_waiting();
+ cx_b.foreground().finish_waiting();
+
+ cx_a.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get no hints due to them turned off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Host should have allowed hint kinds set despite hints are off"
+ );
+ assert_eq!(
+ inlay_cache.version, 0,
+ "Host should not increment its cache version due to no changes",
+ );
+ });
+
+ let mut edits_made = 1;
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string()],
+ extract_hint_labels(editor),
+ "Client should get its first hints when opens an editor"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_language_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx_a.foreground().run_until_parked();
+ editor_a.update(cx_a, |editor, _| {
+ assert!(
+ extract_hint_labels(editor).is_empty(),
+ "Host should get nop hints due to them turned off, even after the /refresh"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 0,
+ "Host should not increment its cache version due to no changes",
+ );
+ });
+
+ edits_made += 1;
+ cx_b.foreground().run_until_parked();
+ editor_b.update(cx_b, |editor, _| {
+ assert_eq!(
+ vec!["0".to_string(), "1".to_string(),],
+ extract_hint_labels(editor),
+ "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Inlay kinds settings never change during the test"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Guest should accepted all edits and bump its cache version every time"
+ );
+ });
+}
+
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,
@@ -7823,3 +8389,17 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
RoomParticipants { remote, pending }
})
}
+
+fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for (_, inlay) in excerpt_hints.hints.iter() {
+ match &inlay.label {
+ project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+ _ => unreachable!(),
+ }
+ }
+ }
+ labels
+}
@@ -1,26 +1,26 @@
mod block_map;
mod fold_map;
-mod suggestion_map;
+mod inlay_map;
mod tab_map;
mod wrap_map;
-use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
-use fold_map::{FoldMap, FoldOffset};
+use fold_map::FoldMap;
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
+use inlay_map::InlayMap;
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
};
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
-pub use suggestion_map::Suggestion;
-use suggestion_map::SuggestionMap;
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
+use text::Rope;
use wrap_map::WrapMap;
pub use block_map::{
@@ -28,6 +28,8 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
+pub use self::inlay_map::{Inlay, InlayProperties};
+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
Folded,
@@ -44,7 +46,7 @@ pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
- suggestion_map: SuggestionMap,
+ inlay_map: InlayMap,
tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap,
@@ -69,8 +71,8 @@ impl DisplayMap {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let tab_size = Self::tab_size(&buffer, cx);
- let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
- let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
+ let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -79,7 +81,7 @@ impl DisplayMap {
buffer,
buffer_subscription,
fold_map,
- suggestion_map,
+ inlay_map,
tab_map,
wrap_map,
block_map,
@@ -88,16 +90,13 @@ impl DisplayMap {
}
}
- pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+ pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
- let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
- let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
-
+ let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
- let (tab_snapshot, edits) = self
- .tab_map
- .sync(suggestion_snapshot.clone(), edits, tab_size);
+ let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
@@ -106,7 +105,7 @@ impl DisplayMap {
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
- suggestion_snapshot,
+ inlay_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
@@ -132,15 +131,14 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -157,15 +155,14 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -181,8 +178,8 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -199,8 +196,8 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -231,32 +228,6 @@ impl DisplayMap {
self.text_highlights.remove(&Some(type_id))
}
- pub fn has_suggestion(&self) -> bool {
- self.suggestion_map.has_suggestion()
- }
-
- pub fn replace_suggestion<T>(
- &self,
- new_suggestion: Option<Suggestion<T>>,
- cx: &mut ModelContext<Self>,
- ) -> Option<Suggestion<FoldOffset>>
- where
- T: ToPoint,
- {
- let snapshot = self.buffer.read(cx).snapshot(cx);
- let edits = self.buffer_subscription.consume().into_inner();
- let tab_size = Self::tab_size(&self.buffer, cx);
- let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits, old_suggestion) =
- self.suggestion_map.replace(new_suggestion, snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
- let (snapshot, edits) = self
- .wrap_map
- .update(cx, |map, cx| map.sync(snapshot, edits, cx));
- self.block_map.read(snapshot, edits);
- old_suggestion
- }
-
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -271,6 +242,39 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlay_map.current_inlays()
+ }
+
+ pub fn splice_inlays<T: Into<Rope>>(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if to_remove.is_empty() && to_insert.is_empty() {
+ return;
+ }
+ let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let edits = self.buffer_subscription.consume().into_inner();
+ let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ self.block_map.read(snapshot, edits);
+
+ let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+ let (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ self.block_map.read(snapshot, edits);
+ }
+
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language = buffer
.read(cx)
@@ -288,7 +292,7 @@ impl DisplayMap {
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
fold_snapshot: fold_map::FoldSnapshot,
- suggestion_snapshot: suggestion_map::SuggestionSnapshot,
+ inlay_snapshot: inlay_map::InlaySnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
@@ -316,9 +320,11 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
- let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
- *fold_point.column_mut() = 0;
- point = fold_point.to_buffer_point(&self.fold_snapshot);
+ let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
+ fold_point.0.column = 0;
+ inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ point = self.inlay_snapshot.to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Left);
*display_point.column_mut() = 0;
@@ -332,9 +338,11 @@ impl DisplaySnapshot {
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
- let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
- *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
- point = fold_point.to_buffer_point(&self.fold_snapshot);
+ let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
+ fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
+ inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ point = self.inlay_snapshot.to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
@@ -364,9 +372,9 @@ impl DisplaySnapshot {
}
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
- let fold_point = self.fold_snapshot.to_fold_point(point, bias);
- let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
- let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
+ let inlay_point = self.inlay_snapshot.to_inlay_point(point);
+ let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+ let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
@@ -376,9 +384,9 @@ impl DisplaySnapshot {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
- let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
- let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_point(&self.fold_snapshot)
+ let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ self.inlay_snapshot.to_buffer_point(inlay_point)
}
pub fn max_point(&self) -> DisplayPoint {
@@ -388,7 +396,13 @@ impl DisplaySnapshot {
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.block_snapshot
- .chunks(display_row..self.max_point().row() + 1, false, None, None)
+ .chunks(
+ display_row..self.max_point().row() + 1,
+ false,
+ None,
+ None,
+ None,
+ )
.map(|h| h.text)
}
@@ -396,7 +410,7 @@ impl DisplaySnapshot {
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.block_snapshot
- .chunks(row..row + 1, false, None, None)
+ .chunks(row..row + 1, false, None, None, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -408,13 +422,15 @@ impl DisplaySnapshot {
&self,
display_rows: Range<u32>,
language_aware: bool,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
display_rows,
language_aware,
Some(&self.text_highlights),
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
)
}
@@ -790,9 +806,10 @@ impl DisplayPoint {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
- let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
- let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_offset(&map.fold_snapshot)
+ let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
+ map.inlay_snapshot
+ .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
}
}
@@ -1706,7 +1723,7 @@ pub mod tests {
) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
- for chunk in snapshot.chunks(rows, true, None) {
+ for chunk in snapshot.chunks(rows, true, None, None) {
let syntax_color = chunk
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);
@@ -573,9 +573,15 @@ impl<'a> BlockMapWriter<'a> {
impl BlockSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(0..self.transforms.summary().output_rows, false, None, None)
- .map(|chunk| chunk.text)
- .collect()
+ self.chunks(
+ 0..self.transforms.summary().output_rows,
+ false,
+ None,
+ None,
+ None,
+ )
+ .map(|chunk| chunk.text)
+ .collect()
}
pub fn chunks<'a>(
@@ -583,7 +589,8 @@ impl BlockSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -616,7 +623,8 @@ impl BlockSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_chunk: Default::default(),
transforms: cursor,
@@ -989,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use super::*;
- use crate::display_map::suggestion_map::SuggestionMap;
+ use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
@@ -1030,9 +1038,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
- let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
@@ -1175,12 +1183,11 @@ mod tests {
buffer.snapshot(cx)
});
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot, subscription.consume().into_inner());
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
+ tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1205,9 +1212,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
@@ -1277,9 +1284,9 @@ mod tests {
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(
@@ -1332,12 +1339,11 @@ mod tests {
})
.collect::<Vec<_>>();
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), vec![]);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1357,12 +1363,11 @@ mod tests {
})
.collect();
- let (fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), vec![]);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1381,11 +1386,10 @@ mod tests {
}
}
- let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
- let (tab_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
@@ -1499,6 +1503,7 @@ mod tests {
false,
None,
None,
+ None,
)
.map(|chunk| chunk.text)
.collect::<String>();
@@ -1,19 +1,15 @@
-use super::TextHighlights;
-use crate::{
- multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
- ToOffset,
+use super::{
+ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+ TextHighlights,
};
-use collections::BTreeMap;
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
use gpui::{color::Color, fonts::HighlightStyle};
use language::{Chunk, Edit, Point, TextSummary};
-use parking_lot::Mutex;
use std::{
any::TypeId,
cmp::{self, Ordering},
- iter::{self, Peekable},
- ops::{Range, Sub},
- sync::atomic::{AtomicUsize, Ordering::SeqCst},
- vec,
+ iter,
+ ops::{Add, AddAssign, Range, Sub},
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
@@ -29,28 +25,24 @@ impl FoldPoint {
self.0.row
}
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+
pub fn row_mut(&mut self) -> &mut u32 {
&mut self.0.row
}
+ #[cfg(test)]
pub fn column_mut(&mut self) -> &mut u32 {
&mut self.0.column
}
- pub fn to_buffer_point(self, snapshot: &FoldSnapshot) -> Point {
- let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
- cursor.seek(&self, Bias::Right, &());
- let overshoot = self.0 - cursor.start().0 .0;
- cursor.start().1 + overshoot
- }
-
- pub fn to_buffer_offset(self, snapshot: &FoldSnapshot) -> usize {
- let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
+ pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+ let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&self, Bias::Right, &());
let overshoot = self.0 - cursor.start().0 .0;
- snapshot
- .buffer_snapshot
- .point_to_offset(cursor.start().1 + overshoot)
+ InlayPoint(cursor.start().1 .0 + overshoot)
}
pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
@@ -63,10 +55,10 @@ impl FoldPoint {
if !overshoot.is_zero() {
let transform = cursor.item().expect("display point out of range");
assert!(transform.output_text.is_none());
- let end_buffer_offset = snapshot
- .buffer_snapshot
- .point_to_offset(cursor.start().1.input.lines + overshoot);
- offset += end_buffer_offset - cursor.start().1.input.len;
+ let end_inlay_offset = snapshot
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+ offset += end_inlay_offset.0 - cursor.start().1.input.len;
}
FoldOffset(offset)
}
@@ -87,8 +79,9 @@ impl<'a> FoldMapWriter<'a> {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
- let buffer = self.0.buffer.lock().clone();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
for range in ranges.into_iter() {
+ let buffer = &snapshot.buffer;
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
// Ignore any empty ranges.
@@ -103,35 +96,32 @@ impl<'a> FoldMapWriter<'a> {
}
folds.push(fold);
- edits.push(text::Edit {
- old: range.clone(),
- new: range,
+
+ let inlay_range =
+ snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
});
}
- folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
+ let buffer = &snapshot.buffer;
+ folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
- self.0.folds = {
+ self.0.snapshot.folds = {
let mut new_tree = SumTree::new();
- let mut cursor = self.0.folds.cursor::<Fold>();
+ let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
for fold in folds {
- new_tree.append(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
- new_tree.push(fold, &buffer);
+ new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+ new_tree.push(fold, buffer);
}
- new_tree.append(cursor.suffix(&buffer), &buffer);
+ new_tree.append(cursor.suffix(buffer), buffer);
new_tree
};
- consolidate_buffer_edits(&mut edits);
- let edits = self.0.sync(buffer.clone(), edits);
- let snapshot = FoldSnapshot {
- transforms: self.0.transforms.lock().clone(),
- folds: self.0.folds.clone(),
- buffer_snapshot: buffer,
- version: self.0.version.load(SeqCst),
- ellipses_color: self.0.ellipses_color,
- };
- (snapshot, edits)
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
}
pub fn unfold<T: ToOffset>(
@@ -141,110 +131,93 @@ impl<'a> FoldMapWriter<'a> {
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
- let buffer = self.0.buffer.lock().clone();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
+ let buffer = &snapshot.buffer;
for range in ranges.into_iter() {
// Remove intersecting folds and add their ranges to edits that are passed to sync.
- let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
+ let mut folds_cursor =
+ intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
while let Some(fold) = folds_cursor.item() {
- let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
+ let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
if offset_range.end > offset_range.start {
- edits.push(text::Edit {
- old: offset_range.clone(),
- new: offset_range,
+ let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+ ..snapshot.to_inlay_offset(offset_range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
});
}
fold_ixs_to_delete.push(*folds_cursor.start());
- folds_cursor.next(&buffer);
+ folds_cursor.next(buffer);
}
}
fold_ixs_to_delete.sort_unstable();
fold_ixs_to_delete.dedup();
- self.0.folds = {
- let mut cursor = self.0.folds.cursor::<usize>();
+ self.0.snapshot.folds = {
+ let mut cursor = self.0.snapshot.folds.cursor::<usize>();
let mut folds = SumTree::new();
for fold_ix in fold_ixs_to_delete {
- folds.append(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
- cursor.next(&buffer);
+ folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+ cursor.next(buffer);
}
- folds.append(cursor.suffix(&buffer), &buffer);
+ folds.append(cursor.suffix(buffer), buffer);
folds
};
- consolidate_buffer_edits(&mut edits);
- let edits = self.0.sync(buffer.clone(), edits);
- let snapshot = FoldSnapshot {
- transforms: self.0.transforms.lock().clone(),
- folds: self.0.folds.clone(),
- buffer_snapshot: buffer,
- version: self.0.version.load(SeqCst),
- ellipses_color: self.0.ellipses_color,
- };
- (snapshot, edits)
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
}
}
pub struct FoldMap {
- buffer: Mutex<MultiBufferSnapshot>,
- transforms: Mutex<SumTree<Transform>>,
- folds: SumTree<Fold>,
- version: AtomicUsize,
+ snapshot: FoldSnapshot,
ellipses_color: Option<Color>,
}
impl FoldMap {
- pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) {
+ pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self {
- buffer: Mutex::new(buffer.clone()),
- folds: Default::default(),
- transforms: Mutex::new(SumTree::from_item(
- Transform {
- summary: TransformSummary {
- input: buffer.text_summary(),
- output: buffer.text_summary(),
+ snapshot: FoldSnapshot {
+ folds: Default::default(),
+ transforms: SumTree::from_item(
+ Transform {
+ summary: TransformSummary {
+ input: inlay_snapshot.text_summary(),
+ output: inlay_snapshot.text_summary(),
+ },
+ output_text: None,
},
- output_text: None,
- },
- &(),
- )),
- ellipses_color: None,
- version: Default::default(),
- };
-
- let snapshot = FoldSnapshot {
- transforms: this.transforms.lock().clone(),
- folds: this.folds.clone(),
- buffer_snapshot: this.buffer.lock().clone(),
- version: this.version.load(SeqCst),
+ &(),
+ ),
+ inlay_snapshot: inlay_snapshot.clone(),
+ version: 0,
+ ellipses_color: None,
+ },
ellipses_color: None,
};
+ let snapshot = this.snapshot.clone();
(this, snapshot)
}
pub fn read(
- &self,
- buffer: MultiBufferSnapshot,
- edits: Vec<Edit<usize>>,
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
- let edits = self.sync(buffer, edits);
+ let edits = self.sync(inlay_snapshot, edits);
self.check_invariants();
- let snapshot = FoldSnapshot {
- transforms: self.transforms.lock().clone(),
- folds: self.folds.clone(),
- buffer_snapshot: self.buffer.lock().clone(),
- version: self.version.load(SeqCst),
- ellipses_color: self.ellipses_color,
- };
- (snapshot, edits)
+ (self.snapshot.clone(), edits)
}
pub fn write(
&mut self,
- buffer: MultiBufferSnapshot,
- edits: Vec<Edit<usize>>,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
- let (snapshot, edits) = self.read(buffer, edits);
+ let (snapshot, edits) = self.read(inlay_snapshot, edits);
(FoldMapWriter(self), snapshot, edits)
}
@@ -260,15 +233,17 @@ impl FoldMap {
fn check_invariants(&self) {
if cfg!(test) {
assert_eq!(
- self.transforms.lock().summary().input.len,
- self.buffer.lock().len(),
- "transform tree does not match buffer's length"
+ self.snapshot.transforms.summary().input.len,
+ self.snapshot.inlay_snapshot.len().0,
+ "transform tree does not match inlay snapshot's length"
);
- let mut folds = self.folds.iter().peekable();
+ let mut folds = self.snapshot.folds.iter().peekable();
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
- let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
+ let comparison = fold
+ .0
+ .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
assert!(comparison.is_le());
}
}
@@ -276,50 +251,42 @@ impl FoldMap {
}
fn sync(
- &self,
- new_buffer: MultiBufferSnapshot,
- buffer_edits: Vec<text::Edit<usize>>,
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ inlay_edits: Vec<InlayEdit>,
) -> Vec<FoldEdit> {
- if buffer_edits.is_empty() {
- let mut buffer = self.buffer.lock();
- if buffer.edit_count() != new_buffer.edit_count()
- || buffer.parse_count() != new_buffer.parse_count()
- || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
- || buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
- || buffer.trailing_excerpt_update_count()
- != new_buffer.trailing_excerpt_update_count()
- {
- self.version.fetch_add(1, SeqCst);
+ if inlay_edits.is_empty() {
+ if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+ self.snapshot.version += 1;
}
- *buffer = new_buffer;
+ self.snapshot.inlay_snapshot = inlay_snapshot;
Vec::new()
} else {
- let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable();
+ let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
let mut new_transforms = SumTree::new();
- let mut transforms = self.transforms.lock();
- let mut cursor = transforms.cursor::<usize>();
- cursor.seek(&0, Bias::Right, &());
+ let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+ cursor.seek(&InlayOffset(0), Bias::Right, &());
- while let Some(mut edit) = buffer_edits_iter.next() {
+ while let Some(mut edit) = inlay_edits_iter.next() {
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
- edit.new.start -= edit.old.start - cursor.start();
+ edit.new.start -= edit.old.start - *cursor.start();
edit.old.start = *cursor.start();
cursor.seek(&edit.old.end, Bias::Right, &());
cursor.next(&());
- let mut delta = edit.new.len() as isize - edit.old.len() as isize;
+ let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
loop {
edit.old.end = *cursor.start();
- if let Some(next_edit) = buffer_edits_iter.peek() {
+ if let Some(next_edit) = inlay_edits_iter.peek() {
if next_edit.old.start > edit.old.end {
break;
}
- let next_edit = buffer_edits_iter.next().unwrap();
- delta += next_edit.new.len() as isize - next_edit.old.len() as isize;
+ let next_edit = inlay_edits_iter.next().unwrap();
+ delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
if next_edit.old.end >= edit.old.end {
edit.old.end = next_edit.old.end;
@@ -331,19 +298,29 @@ impl FoldMap {
}
}
- edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize;
-
- let anchor = new_buffer.anchor_before(edit.new.start);
- let mut folds_cursor = self.folds.cursor::<Fold>();
- folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &new_buffer);
+ edit.new.end =
+ InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+ let anchor = inlay_snapshot
+ .buffer
+ .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+ let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+ folds_cursor.seek(
+ &Fold(anchor..Anchor::max()),
+ Bias::Left,
+ &inlay_snapshot.buffer,
+ );
let mut folds = iter::from_fn({
- let buffer = &new_buffer;
+ let inlay_snapshot = &inlay_snapshot;
move || {
- let item = folds_cursor
- .item()
- .map(|f| f.0.start.to_offset(buffer)..f.0.end.to_offset(buffer));
- folds_cursor.next(buffer);
+ let item = folds_cursor.item().map(|f| {
+ let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+ let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+ inlay_snapshot.to_inlay_offset(buffer_start)
+ ..inlay_snapshot.to_inlay_offset(buffer_end)
+ });
+ folds_cursor.next(&inlay_snapshot.buffer);
item
}
})
@@ -353,7 +330,7 @@ impl FoldMap {
let mut fold = folds.next().unwrap();
let sum = new_transforms.summary();
- assert!(fold.start >= sum.input.len);
+ assert!(fold.start.0 >= sum.input.len);
while folds
.peek()
@@ -365,9 +342,9 @@ impl FoldMap {
}
}
- if fold.start > sum.input.len {
- let text_summary = new_buffer
- .text_summary_for_range::<TextSummary, _>(sum.input.len..fold.start);
+ if fold.start.0 > sum.input.len {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -386,7 +363,8 @@ impl FoldMap {
Transform {
summary: TransformSummary {
output: TextSummary::from(output_text),
- input: new_buffer.text_summary_for_range(fold.start..fold.end),
+ input: inlay_snapshot
+ .text_summary_for_range(fold.start..fold.end),
},
output_text: Some(output_text),
},
@@ -396,9 +374,9 @@ impl FoldMap {
}
let sum = new_transforms.summary();
- if sum.input.len < edit.new.end {
- let text_summary = new_buffer
- .text_summary_for_range::<TextSummary, _>(sum.input.len..edit.new.end);
+ if sum.input.len < edit.new.end.0 {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -414,7 +392,7 @@ impl FoldMap {
new_transforms.append(cursor.suffix(&()), &());
if new_transforms.is_empty() {
- let text_summary = new_buffer.text_summary();
+ let text_summary = inlay_snapshot.text_summary();
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -429,18 +407,21 @@ impl FoldMap {
drop(cursor);
- let mut fold_edits = Vec::with_capacity(buffer_edits.len());
+ let mut fold_edits = Vec::with_capacity(inlay_edits.len());
{
- let mut old_transforms = transforms.cursor::<(usize, FoldOffset)>();
- let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>();
+ let mut old_transforms = self
+ .snapshot
+ .transforms
+ .cursor::<(InlayOffset, FoldOffset)>();
+ let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
- for mut edit in buffer_edits {
+ for mut edit in inlay_edits {
old_transforms.seek(&edit.old.start, Bias::Left, &());
if old_transforms.item().map_or(false, |t| t.is_fold()) {
edit.old.start = old_transforms.start().0;
}
let old_start =
- old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0);
+ old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
if old_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -448,14 +429,14 @@ impl FoldMap {
edit.old.end = old_transforms.start().0;
}
let old_end =
- old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0);
+ old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
new_transforms.seek(&edit.new.start, Bias::Left, &());
if new_transforms.item().map_or(false, |t| t.is_fold()) {
edit.new.start = new_transforms.start().0;
}
let new_start =
- new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0);
+ new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
if new_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -463,7 +444,7 @@ impl FoldMap {
edit.new.end = new_transforms.start().0;
}
let new_end =
- new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
+ new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
fold_edits.push(FoldEdit {
old: FoldOffset(old_start)..FoldOffset(old_end),
@@ -474,9 +455,9 @@ impl FoldMap {
consolidate_fold_edits(&mut fold_edits);
}
- *transforms = new_transforms;
- *self.buffer.lock() = new_buffer;
- self.version.fetch_add(1, SeqCst);
+ self.snapshot.transforms = new_transforms;
+ self.snapshot.inlay_snapshot = inlay_snapshot;
+ self.snapshot.version += 1;
fold_edits
}
}
@@ -486,32 +467,28 @@ impl FoldMap {
pub struct FoldSnapshot {
transforms: SumTree<Transform>,
folds: SumTree<Fold>,
- buffer_snapshot: MultiBufferSnapshot,
+ pub inlay_snapshot: InlaySnapshot,
pub version: usize,
pub ellipses_color: Option<Color>,
}
impl FoldSnapshot {
- pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- &self.buffer_snapshot
- }
-
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(FoldOffset(0)..self.len(), false, None)
+ self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
.map(|c| c.text)
.collect()
}
#[cfg(test)]
pub fn fold_count(&self) -> usize {
- self.folds.items(&self.buffer_snapshot).len()
+ self.folds.items(&self.inlay_snapshot.buffer).len()
}
pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
let mut summary = TextSummary::default();
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&range.start, Bias::Right, &());
if let Some(transform) = cursor.item() {
let start_in_transform = range.start.0 - cursor.start().0 .0;
@@ -522,11 +499,15 @@ impl FoldSnapshot {
[start_in_transform.column as usize..end_in_transform.column as usize],
);
} else {
- let buffer_start = cursor.start().1 + start_in_transform;
- let buffer_end = cursor.start().1 + end_in_transform;
+ let inlay_start = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
summary = self
- .buffer_snapshot
- .text_summary_for_range(buffer_start..buffer_end);
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
}
}
@@ -540,11 +521,13 @@ impl FoldSnapshot {
if let Some(output_text) = transform.output_text {
summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
} else {
- let buffer_start = cursor.start().1;
- let buffer_end = cursor.start().1 + end_in_transform;
+ let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
summary += self
- .buffer_snapshot
- .text_summary_for_range::<TextSummary, _>(buffer_start..buffer_end);
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
}
}
}
@@ -552,8 +535,8 @@ impl FoldSnapshot {
summary
}
- pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
- let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
+ pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
cursor.seek(&point, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || point == cursor.start().0 {
@@ -562,7 +545,7 @@ impl FoldSnapshot {
cursor.end(&()).1
}
} else {
- let overshoot = point - cursor.start().0;
+ let overshoot = point.0 - cursor.start().0 .0;
FoldPoint(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
@@ -590,12 +573,12 @@ impl FoldSnapshot {
}
let fold_point = FoldPoint::new(start_row, 0);
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&fold_point, Bias::Left, &());
let overshoot = fold_point.0 - cursor.start().0 .0;
- let buffer_point = cursor.start().1 + overshoot;
- let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row);
+ let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+ let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
FoldBufferRows {
fold_point,
@@ -617,10 +600,10 @@ impl FoldSnapshot {
where
T: ToOffset,
{
- let mut folds = intersecting_folds(&self.buffer_snapshot, &self.folds, range, false);
+ let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item().map(|f| &f.0);
- folds.next(&self.buffer_snapshot);
+ folds.next(&self.inlay_snapshot.buffer);
item
})
}
@@ -629,26 +612,39 @@ impl FoldSnapshot {
where
T: ToOffset,
{
- let offset = offset.to_offset(&self.buffer_snapshot);
- let mut cursor = self.transforms.cursor::<usize>();
- cursor.seek(&offset, Bias::Right, &());
+ let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+ let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+ let mut cursor = self.transforms.cursor::<InlayOffset>();
+ cursor.seek(&inlay_offset, Bias::Right, &());
cursor.item().map_or(false, |t| t.output_text.is_some())
}
pub fn is_line_folded(&self, buffer_row: u32) -> bool {
- let mut cursor = self.transforms.cursor::<Point>();
- cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
- while let Some(transform) = cursor.item() {
- if transform.output_text.is_some() {
- return true;
+ let mut inlay_point = self
+ .inlay_snapshot
+ .to_inlay_point(Point::new(buffer_row, 0));
+ let mut cursor = self.transforms.cursor::<InlayPoint>();
+ cursor.seek(&inlay_point, Bias::Right, &());
+ loop {
+ match cursor.item() {
+ Some(transform) => {
+ let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+ if buffer_point.row != buffer_row {
+ return false;
+ } else if transform.output_text.is_some() {
+ return true;
+ }
+ }
+ None => return false,
}
- if cursor.end(&()).row == buffer_row {
- cursor.next(&())
+
+ if cursor.end(&()).row() == inlay_point.row() {
+ cursor.next(&());
} else {
- break;
+ inlay_point.0 += Point::new(1, 0);
+ cursor.seek(&inlay_point, Bias::Right, &());
}
}
- false
}
pub fn chunks<'a>(
@@ -656,127 +652,56 @@ impl FoldSnapshot {
range: Range<FoldOffset>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> FoldChunks<'a> {
- let mut highlight_endpoints = Vec::new();
- let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
+ let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
- let buffer_end = {
+ let inlay_end = {
transform_cursor.seek(&range.end, Bias::Right, &());
let overshoot = range.end.0 - transform_cursor.start().0 .0;
- transform_cursor.start().1 + overshoot
+ transform_cursor.start().1 + InlayOffset(overshoot)
};
- let buffer_start = {
+ let inlay_start = {
transform_cursor.seek(&range.start, Bias::Right, &());
let overshoot = range.start.0 - transform_cursor.start().0 .0;
- transform_cursor.start().1 + overshoot
+ transform_cursor.start().1 + InlayOffset(overshoot)
};
- if let Some(text_highlights) = text_highlights {
- if !text_highlights.is_empty() {
- while transform_cursor.start().0 < range.end {
- if !transform_cursor.item().unwrap().is_fold() {
- let transform_start = self
- .buffer_snapshot
- .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
-
- let transform_end = {
- let overshoot = range.end.0 - transform_cursor.start().0 .0;
- self.buffer_snapshot.anchor_before(cmp::min(
- transform_cursor.end(&()).1,
- transform_cursor.start().1 + overshoot,
- ))
- };
-
- for (tag, highlights) in text_highlights.iter() {
- let style = highlights.0;
- let ranges = &highlights.1;
-
- let start_ix = match ranges.binary_search_by(|probe| {
- let cmp = probe.end.cmp(&transform_start, self.buffer_snapshot());
- if cmp.is_gt() {
- Ordering::Greater
- } else {
- Ordering::Less
- }
- }) {
- Ok(i) | Err(i) => i,
- };
- for range in &ranges[start_ix..] {
- if range
- .start
- .cmp(&transform_end, &self.buffer_snapshot)
- .is_ge()
- {
- break;
- }
-
- highlight_endpoints.push(HighlightEndpoint {
- offset: range.start.to_offset(&self.buffer_snapshot),
- is_start: true,
- tag: *tag,
- style,
- });
- highlight_endpoints.push(HighlightEndpoint {
- offset: range.end.to_offset(&self.buffer_snapshot),
- is_start: false,
- tag: *tag,
- style,
- });
- }
- }
- }
-
- transform_cursor.next(&());
- }
- highlight_endpoints.sort();
- transform_cursor.seek(&range.start, Bias::Right, &());
- }
- }
-
FoldChunks {
transform_cursor,
- buffer_chunks: self
- .buffer_snapshot
- .chunks(buffer_start..buffer_end, language_aware),
- buffer_chunk: None,
- buffer_offset: buffer_start,
+ inlay_chunks: self.inlay_snapshot.chunks(
+ inlay_start..inlay_end,
+ language_aware,
+ text_highlights,
+ hint_highlights,
+ suggestion_highlights,
+ ),
+ inlay_chunk: None,
+ inlay_offset: inlay_start,
output_offset: range.start.0,
max_output_offset: range.end.0,
- highlight_endpoints: highlight_endpoints.into_iter().peekable(),
- active_highlights: Default::default(),
ellipses_color: self.ellipses_color,
}
}
+ pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+ self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
+ .flat_map(|chunk| chunk.text.chars())
+ }
+
#[cfg(test)]
pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
- let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>();
- cursor.seek(&offset, Bias::Right, &());
- if let Some(transform) = cursor.item() {
- let transform_start = cursor.start().0 .0;
- if transform.output_text.is_some() {
- if offset.0 == transform_start || matches!(bias, Bias::Left) {
- FoldOffset(transform_start)
- } else {
- FoldOffset(cursor.end(&()).0 .0)
- }
- } else {
- let overshoot = offset.0 - transform_start;
- let buffer_offset = cursor.start().1 + overshoot;
- let clipped_buffer_offset = self.buffer_snapshot.clip_offset(buffer_offset, bias);
- FoldOffset(
- (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize))
- as usize,
- )
- }
+ if offset > self.len() {
+ self.len()
} else {
- FoldOffset(self.transforms.summary().output.len)
+ self.clip_point(offset.to_point(self), bias).to_offset(self)
}
}
pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
- let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
cursor.seek(&point, Bias::Right, &());
if let Some(transform) = cursor.item() {
let transform_start = cursor.start().0 .0;
@@ -787,11 +712,10 @@ impl FoldSnapshot {
FoldPoint(cursor.end(&()).0 .0)
}
} else {
- let overshoot = point.0 - transform_start;
- let buffer_position = cursor.start().1 + overshoot;
- let clipped_buffer_position =
- self.buffer_snapshot.clip_point(buffer_position, bias);
- FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1))
+ let overshoot = InlayPoint(point.0 - transform_start);
+ let inlay_point = cursor.start().1 + overshoot;
+ let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+ FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
}
} else {
FoldPoint(self.transforms.summary().output.lines)
@@ -800,7 +724,7 @@ impl FoldSnapshot {
}
fn intersecting_folds<'a, T>(
- buffer: &'a MultiBufferSnapshot,
+ inlay_snapshot: &'a InlaySnapshot,
folds: &'a SumTree<Fold>,
range: Range<T>,
inclusive: bool,
@@ -808,6 +732,7 @@ fn intersecting_folds<'a, T>(
where
T: ToOffset,
{
+ let buffer = &inlay_snapshot.buffer;
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(move |summary| {
@@ -824,7 +749,7 @@ where
cursor
}
-fn consolidate_buffer_edits(edits: &mut Vec<text::Edit<usize>>) {
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
edits.sort_unstable_by(|a, b| {
a.old
.start
@@ -952,7 +877,7 @@ impl Default for FoldSummary {
impl sum_tree::Summary for FoldSummary {
type Context = MultiBufferSnapshot;
- fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
+ fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
self.min_start = other.min_start.clone();
}
@@ -996,8 +921,8 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
#[derive(Clone)]
pub struct FoldBufferRows<'a> {
- cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
- input_buffer_rows: MultiBufferRows<'a>,
+ cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+ input_buffer_rows: InlayBufferRows<'a>,
fold_point: FoldPoint,
}
@@ -1016,7 +941,7 @@ impl<'a> Iterator for FoldBufferRows<'a> {
if self.cursor.item().is_some() {
if traversed_fold {
- self.input_buffer_rows.seek(self.cursor.start().1.row);
+ self.input_buffer_rows.seek(self.cursor.start().1.row());
self.input_buffer_rows.next();
}
*self.fold_point.row_mut() += 1;
@@ -1028,14 +953,12 @@ impl<'a> Iterator for FoldBufferRows<'a> {
}
pub struct FoldChunks<'a> {
- transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
- buffer_chunks: MultiBufferChunks<'a>,
- buffer_chunk: Option<(usize, Chunk<'a>)>,
- buffer_offset: usize,
+ transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+ inlay_chunks: InlayChunks<'a>,
+ inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+ inlay_offset: InlayOffset,
output_offset: usize,
max_output_offset: usize,
- highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
- active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
ellipses_color: Option<Color>,
}
@@ -1052,11 +975,11 @@ impl<'a> Iterator for FoldChunks<'a> {
// If we're in a fold, then return the fold's display text and
// advance the transform and buffer cursors to the end of the fold.
if let Some(output_text) = transform.output_text {
- self.buffer_chunk.take();
- self.buffer_offset += transform.summary.input.len;
- self.buffer_chunks.seek(self.buffer_offset);
+ self.inlay_chunk.take();
+ self.inlay_offset += InlayOffset(transform.summary.input.len);
+ self.inlay_chunks.seek(self.inlay_offset);
- while self.buffer_offset >= self.transform_cursor.end(&()).1
+ while self.inlay_offset >= self.transform_cursor.end(&()).1
&& self.transform_cursor.item().is_some()
{
self.transform_cursor.next(&());
@@ -1073,53 +996,28 @@ impl<'a> Iterator for FoldChunks<'a> {
});
}
- let mut next_highlight_endpoint = usize::MAX;
- while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
- if endpoint.offset <= self.buffer_offset {
- if endpoint.is_start {
- self.active_highlights.insert(endpoint.tag, endpoint.style);
- } else {
- self.active_highlights.remove(&endpoint.tag);
- }
- self.highlight_endpoints.next();
- } else {
- next_highlight_endpoint = endpoint.offset;
- break;
- }
- }
-
// Retrieve a chunk from the current location in the buffer.
- if self.buffer_chunk.is_none() {
- let chunk_offset = self.buffer_chunks.offset();
- self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
+ if self.inlay_chunk.is_none() {
+ let chunk_offset = self.inlay_chunks.offset();
+ self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
}
// Otherwise, take a chunk from the buffer's text.
- if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
- let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+ if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+ let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
- let chunk_end = buffer_chunk_end
- .min(transform_end)
- .min(next_highlight_endpoint);
+ let chunk_end = buffer_chunk_end.min(transform_end);
chunk.text = &chunk.text
- [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
-
- if !self.active_highlights.is_empty() {
- let mut highlight_style = HighlightStyle::default();
- for active_highlight in self.active_highlights.values() {
- highlight_style.highlight(*active_highlight);
- }
- chunk.highlight_style = Some(highlight_style);
- }
+ [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
if chunk_end == transform_end {
self.transform_cursor.next(&());
} else if chunk_end == buffer_chunk_end {
- self.buffer_chunk.take();
+ self.inlay_chunk.take();
}
- self.buffer_offset = chunk_end;
+ self.inlay_offset = chunk_end;
self.output_offset += chunk.text.len();
return Some(chunk);
}
@@ -0,0 +1,1697 @@
+use crate::{
+ multi_buffer::{MultiBufferChunks, MultiBufferRows},
+ Anchor, InlayId, MultiBufferSnapshot, ToOffset,
+};
+use collections::{BTreeMap, BTreeSet, HashMap};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, Rope, TextSummary};
+use std::{
+ any::TypeId,
+ cmp,
+ iter::Peekable,
+ ops::{Add, AddAssign, Range, Sub, SubAssign},
+ vec,
+};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::Patch;
+
+use super::TextHighlights;
+
+pub struct InlayMap {
+ snapshot: InlaySnapshot,
+ inlays_by_id: HashMap<InlayId, Inlay>,
+ inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+ pub buffer: MultiBufferSnapshot,
+ transforms: SumTree<Transform>,
+ pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+ Isomorphic(TextSummary),
+ Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+ pub id: InlayId,
+ pub position: Anchor,
+ pub text: text::Rope,
+}
+
+#[derive(Debug, Clone)]
+pub struct InlayProperties<T> {
+ pub position: Anchor,
+ pub text: T,
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ match self {
+ Transform::Isomorphic(summary) => TransformSummary {
+ input: summary.clone(),
+ output: summary.clone(),
+ },
+ Transform::Inlay(inlay) => TransformSummary {
+ input: TextSummary::default(),
+ output: inlay.text.summary(),
+ },
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+ input: TextSummary,
+ output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, other: &Self, _: &()) {
+ self.input += &other.input;
+ self.output += &other.output;
+ }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayOffset {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl AddAssign for InlayOffset {
+ fn add_assign(&mut self, rhs: Self) {
+ self.0 += rhs.0;
+ }
+}
+
+impl SubAssign for InlayOffset {
+ fn sub_assign(&mut self, rhs: Self) {
+ self.0 -= rhs.0;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.len;
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayPoint {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.lines;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.len;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.lines;
+ }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+ transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+ buffer_rows: MultiBufferRows<'a>,
+ inlay_row: u32,
+ max_buffer_row: u32,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+ offset: InlayOffset,
+ is_start: bool,
+ tag: Option<TypeId>,
+ style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for HighlightEndpoint {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.offset
+ .cmp(&other.offset)
+ .then_with(|| other.is_start.cmp(&self.is_start))
+ }
+}
+
+pub struct InlayChunks<'a> {
+ transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+ buffer_chunks: MultiBufferChunks<'a>,
+ buffer_chunk: Option<Chunk<'a>>,
+ inlay_chunks: Option<text::Chunks<'a>>,
+ output_offset: InlayOffset,
+ max_output_offset: InlayOffset,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
+ highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+ active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+ snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+ pub fn seek(&mut self, offset: InlayOffset) {
+ self.transforms.seek(&offset, Bias::Right, &());
+
+ let buffer_offset = self.snapshot.to_buffer_offset(offset);
+ self.buffer_chunks.seek(buffer_offset);
+ self.inlay_chunks = None;
+ self.buffer_chunk = None;
+ self.output_offset = offset;
+ }
+
+ pub fn offset(&self) -> InlayOffset {
+ self.output_offset
+ }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_offset == self.max_output_offset {
+ return None;
+ }
+
+ let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+ while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+ if endpoint.offset <= self.output_offset {
+ if endpoint.is_start {
+ self.active_highlights.insert(endpoint.tag, endpoint.style);
+ } else {
+ self.active_highlights.remove(&endpoint.tag);
+ }
+ self.highlight_endpoints.next();
+ } else {
+ next_highlight_endpoint = endpoint.offset;
+ break;
+ }
+ }
+
+ let chunk = match self.transforms.item()? {
+ Transform::Isomorphic(_) => {
+ let chunk = self
+ .buffer_chunk
+ .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+ if chunk.text.is_empty() {
+ *chunk = self.buffer_chunks.next().unwrap();
+ }
+
+ let (prefix, suffix) = chunk.text.split_at(
+ chunk
+ .text
+ .len()
+ .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+ .min(next_highlight_endpoint.0 - self.output_offset.0),
+ );
+
+ chunk.text = suffix;
+ self.output_offset.0 += prefix.len();
+ let mut prefix = Chunk {
+ text: prefix,
+ ..chunk.clone()
+ };
+ if !self.active_highlights.is_empty() {
+ let mut highlight_style = HighlightStyle::default();
+ for active_highlight in self.active_highlights.values() {
+ highlight_style.highlight(*active_highlight);
+ }
+ prefix.highlight_style = Some(highlight_style);
+ }
+ prefix
+ }
+ Transform::Inlay(inlay) => {
+ let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+ let start = self.output_offset - self.transforms.start().0;
+ let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
+ - self.transforms.start().0;
+ inlay.text.chunks_in_range(start.0..end.0)
+ });
+
+ let chunk = inlay_chunks.next().unwrap();
+ self.output_offset.0 += chunk.len();
+ let highlight_style = match inlay.id {
+ InlayId::Suggestion(_) => self.suggestion_highlight_style,
+ InlayId::Hint(_) => self.hint_highlight_style,
+ };
+ Chunk {
+ text: chunk,
+ highlight_style,
+ ..Default::default()
+ }
+ }
+ };
+
+ if self.output_offset == self.transforms.end(&()).0 {
+ self.inlay_chunks = None;
+ self.transforms.next(&());
+ }
+
+ Some(chunk)
+ }
+}
+
+impl<'a> InlayBufferRows<'a> {
+ pub fn seek(&mut self, row: u32) {
+ let inlay_point = InlayPoint::new(row, 0);
+ self.transforms.seek(&inlay_point, Bias::Left, &());
+
+ let mut buffer_point = self.transforms.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match self.transforms.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+ }
+ };
+ self.inlay_row = inlay_point.row();
+ self.buffer_rows.seek(buffer_row);
+ }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let buffer_row = if self.inlay_row == 0 {
+ self.buffer_rows.next().unwrap()
+ } else {
+ match self.transforms.item()? {
+ Transform::Inlay(_) => None,
+ Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+ }
+ };
+
+ self.inlay_row += 1;
+ self.transforms
+ .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+ Some(buffer_row)
+ }
+}
+
+impl InlayPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+}
+
+impl InlayMap {
+ pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+ let version = 0;
+ let snapshot = InlaySnapshot {
+ buffer: buffer.clone(),
+ transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+ version,
+ };
+
+ (
+ Self {
+ snapshot: snapshot.clone(),
+ inlays_by_id: HashMap::default(),
+ inlays: Vec::new(),
+ },
+ snapshot,
+ )
+ }
+
+ pub fn sync(
+ &mut self,
+ buffer_snapshot: MultiBufferSnapshot,
+ mut buffer_edits: Vec<text::Edit<usize>>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let mut snapshot = &mut self.snapshot;
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ buffer_edits.push(Edit {
+ old: snapshot.buffer.len()..snapshot.buffer.len(),
+ new: buffer_snapshot.len()..buffer_snapshot.len(),
+ });
+ }
+ }
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+ || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+ || snapshot.buffer.diagnostics_update_count()
+ != buffer_snapshot.diagnostics_update_count()
+ || snapshot.buffer.git_diff_update_count()
+ != buffer_snapshot.git_diff_update_count()
+ || snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ snapshot.version += 1;
+ }
+
+ snapshot.buffer = buffer_snapshot;
+ (snapshot.clone(), Vec::new())
+ } else {
+ let mut inlay_edits = Patch::default();
+ let mut new_transforms = SumTree::new();
+ let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+ let mut buffer_edits_iter = buffer_edits.iter().peekable();
+ while let Some(buffer_edit) = buffer_edits_iter.next() {
+ new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+ if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+ if cursor.end(&()).0 == buffer_edit.old.start {
+ push_isomorphic(&mut new_transforms, transform.clone());
+ cursor.next(&());
+ }
+ }
+
+ // Remove all the inlays and transforms contained by the edit.
+ let old_start =
+ cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+ cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+ let old_end =
+ cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+ // Push the unchanged prefix.
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_edit.new.start;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+ let new_start = InlayOffset(new_transforms.summary().output.len);
+
+ let start_ix = match self.inlays.binary_search_by(|probe| {
+ probe
+ .position
+ .to_offset(&buffer_snapshot)
+ .cmp(&buffer_edit.new.start)
+ .then(std::cmp::Ordering::Greater)
+ }) {
+ Ok(ix) | Err(ix) => ix,
+ };
+
+ for inlay in &self.inlays[start_ix..] {
+ let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+ if buffer_offset > buffer_edit.new.end {
+ break;
+ }
+
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_offset;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+
+ if inlay.position.is_valid(&buffer_snapshot) {
+ new_transforms.push(Transform::Inlay(inlay.clone()), &());
+ }
+ }
+
+ // Apply the rest of the edit.
+ let transform_start = new_transforms.summary().input.len;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+ );
+ let new_end = InlayOffset(new_transforms.summary().output.len);
+ inlay_edits.push(Edit {
+ old: old_start..old_end,
+ new: new_start..new_end,
+ });
+
+ // If the next edit doesn't intersect the current isomorphic transform, then
+ // we can push its remainder.
+ if buffer_edits_iter
+ .peek()
+ .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+ {
+ let transform_start = new_transforms.summary().input.len;
+ let transform_end =
+ buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+ );
+ cursor.next(&());
+ }
+ }
+
+ new_transforms.append(cursor.suffix(&()), &());
+ if new_transforms.is_empty() {
+ new_transforms.push(Transform::Isomorphic(Default::default()), &());
+ }
+
+ drop(cursor);
+ snapshot.transforms = new_transforms;
+ snapshot.version += 1;
+ snapshot.buffer = buffer_snapshot;
+ snapshot.check_invariants();
+
+ (snapshot.clone(), inlay_edits.into_inner())
+ }
+ }
+
+ pub fn splice<T: Into<Rope>>(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let snapshot = &mut self.snapshot;
+ let mut edits = BTreeSet::new();
+
+ self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
+ for inlay_id in to_remove {
+ if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
+ let offset = inlay.position.to_offset(&snapshot.buffer);
+ edits.insert(offset);
+ }
+ }
+
+ for (existing_id, properties) in to_insert {
+ let inlay = Inlay {
+ id: existing_id,
+ position: properties.position,
+ text: properties.text.into(),
+ };
+
+ // Avoid inserting empty inlays.
+ if inlay.text.is_empty() {
+ continue;
+ }
+
+ self.inlays_by_id.insert(inlay.id, inlay.clone());
+ match self
+ .inlays
+ .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
+ {
+ Ok(ix) | Err(ix) => {
+ self.inlays.insert(ix, inlay.clone());
+ }
+ }
+
+ let offset = inlay.position.to_offset(&snapshot.buffer);
+ edits.insert(offset);
+ }
+
+ let buffer_edits = edits
+ .into_iter()
+ .map(|offset| Edit {
+ old: offset..offset,
+ new: offset..offset,
+ })
+ .collect();
+ let buffer_snapshot = snapshot.buffer.clone();
+ drop(snapshot);
+ let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+ (snapshot, edits)
+ }
+
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlays.iter()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn randomly_mutate(
+ &mut self,
+ next_inlay_id: &mut usize,
+ rng: &mut rand::rngs::StdRng,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ use rand::prelude::*;
+ use util::post_inc;
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let snapshot = &mut self.snapshot;
+ for i in 0..rng.gen_range(1..=5) {
+ if self.inlays.is_empty() || rng.gen() {
+ let position = snapshot.buffer.random_byte_range(0, rng).start;
+ let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+ let len = if rng.gen_bool(0.01) {
+ 0
+ } else {
+ rng.gen_range(1..=5)
+ };
+ let text = util::RandomCharIter::new(&mut *rng)
+ .filter(|ch| *ch != '\r')
+ .take(len)
+ .collect::<String>();
+ log::info!(
+ "creating inlay at buffer offset {} with bias {:?} and text {:?}",
+ position,
+ bias,
+ text
+ );
+
+ let inlay_id = if i % 2 == 0 {
+ InlayId::Hint(post_inc(next_inlay_id))
+ } else {
+ InlayId::Suggestion(post_inc(next_inlay_id))
+ };
+ to_insert.push((
+ inlay_id,
+ InlayProperties {
+ position: snapshot.buffer.anchor_at(position, bias),
+ text,
+ },
+ ));
+ } else {
+ to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
+ }
+ }
+ log::info!("removing inlays: {:?}", to_remove);
+
+ drop(snapshot);
+ let (snapshot, edits) = self.splice(to_remove, to_insert);
+ (snapshot, edits)
+ }
+}
+
+impl InlaySnapshot {
+ pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayOffset, (InlayPoint, usize))>();
+ cursor.seek(&offset, Bias::Right, &());
+ let overshoot = offset.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_offset_start = cursor.start().1 .1;
+ let buffer_offset_end = buffer_offset_start + overshoot;
+ let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+ let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+ InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.offset_to_point(overshoot);
+ InlayPoint(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.max_point(),
+ }
+ }
+
+ pub fn len(&self) -> InlayOffset {
+ InlayOffset(self.transforms.summary().output.len)
+ }
+
+ pub fn max_point(&self) -> InlayPoint {
+ InlayPoint(self.transforms.summary().output.lines)
+ }
+
+ pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayPoint, (InlayOffset, Point))>();
+ cursor.seek(&point, Bias::Right, &());
+ let overshoot = point.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_point_start = cursor.start().1 .1;
+ let buffer_point_end = buffer_point_start + overshoot;
+ let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+ let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+ InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.point_to_offset(overshoot);
+ InlayOffset(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.len(),
+ }
+ }
+
+ pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = point.0 - cursor.start().0 .0;
+ cursor.start().1 + overshoot
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.max_point(),
+ }
+ }
+
+ pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&offset, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = offset - cursor.start().0;
+ cursor.start().1 + overshoot.0
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.len(),
+ }
+ }
+
+ pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+ let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+ cursor.seek(&offset, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if offset == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = offset - cursor.start().0;
+ return InlayOffset(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.len();
+ }
+ }
+ }
+ }
+
+ pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if point == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = point - cursor.start().0;
+ return InlayPoint(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.max_point();
+ }
+ }
+ }
+ }
+
+ pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(transform)) => {
+ if cursor.start().0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ } else if bias == Bias::Left {
+ cursor.prev(&());
+ } else if transform.first_line_chars == 0 {
+ point.0 += Point::new(1, 0);
+ } else {
+ point.0 += Point::new(0, 1);
+ }
+ } else {
+ return point;
+ }
+ } else if cursor.end(&()).0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ } else if bias == Bias::Right {
+ cursor.next(&());
+ } else if point.0.column == 0 {
+ point.0.row -= 1;
+ point.0.column = self.line_len(point.0.row);
+ } else {
+ point.0.column -= 1;
+ }
+ } else {
+ return point;
+ }
+ } else {
+ let overshoot = point.0 - cursor.start().0 .0;
+ let buffer_point = cursor.start().1 + overshoot;
+ let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+ let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+ let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+ if clipped_point == point {
+ return clipped_point;
+ } else {
+ point = clipped_point;
+ }
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+ match cursor.prev_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+ match cursor.next_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ }
+
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ None => {
+ bias = bias.invert();
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ }
+ }
+ }
+
+ pub fn text_summary(&self) -> TextSummary {
+ self.transforms.summary().output.clone()
+ }
+
+ pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+ let mut summary = TextSummary::default();
+
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let overshoot = range.start.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_start = cursor.start().1;
+ let suffix_start = buffer_start + overshoot;
+ let suffix_end =
+ buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+ summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+ cursor.next(&());
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let suffix_start = overshoot;
+ let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+ summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+ cursor.next(&());
+ }
+ None => {}
+ }
+
+ if range.end > cursor.start().0 {
+ summary += cursor
+ .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+ .output;
+
+ let overshoot = range.end.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let prefix_start = cursor.start().1;
+ let prefix_end = prefix_start + overshoot;
+ summary += self
+ .buffer
+ .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let prefix_end = overshoot;
+ summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+ }
+ None => {}
+ }
+ }
+
+ summary
+ }
+
+ pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ let inlay_point = InlayPoint::new(row, 0);
+ cursor.seek(&inlay_point, Bias::Left, &());
+
+ let max_buffer_row = self.buffer.max_point().row;
+ let mut buffer_point = cursor.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - cursor.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+ }
+ };
+
+ InlayBufferRows {
+ transforms: cursor,
+ inlay_row: inlay_point.row(),
+ buffer_rows: self.buffer.buffer_rows(buffer_row),
+ max_buffer_row,
+ }
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+ let line_end = if row >= self.max_point().row() {
+ self.len().0
+ } else {
+ self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+ };
+ (line_end - line_start) as u32
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ range: Range<InlayOffset>,
+ language_aware: bool,
+ text_highlights: Option<&'a TextHighlights>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
+ ) -> InlayChunks<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let mut highlight_endpoints = Vec::new();
+ if let Some(text_highlights) = text_highlights {
+ if !text_highlights.is_empty() {
+ while cursor.start().0 < range.end {
+ if true {
+ let transform_start = self.buffer.anchor_after(
+ self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
+ );
+
+ let transform_end = {
+ let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+ self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+ cursor.end(&()).0,
+ cursor.start().0 + overshoot,
+ )))
+ };
+
+ for (tag, highlights) in text_highlights.iter() {
+ let style = highlights.0;
+ let ranges = &highlights.1;
+
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe.end.cmp(&transform_start, &self.buffer);
+ if cmp.is_gt() {
+ cmp::Ordering::Greater
+ } else {
+ cmp::Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ for range in &ranges[start_ix..] {
+ if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+ break;
+ }
+
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self
+ .to_inlay_offset(range.start.to_offset(&self.buffer)),
+ is_start: true,
+ tag: *tag,
+ style,
+ });
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+ is_start: false,
+ tag: *tag,
+ style,
+ });
+ }
+ }
+ }
+
+ cursor.next(&());
+ }
+ highlight_endpoints.sort();
+ cursor.seek(&range.start, Bias::Right, &());
+ }
+ }
+
+ let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
+ let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
+
+ InlayChunks {
+ transforms: cursor,
+ buffer_chunks,
+ inlay_chunks: None,
+ buffer_chunk: None,
+ output_offset: range.start,
+ max_output_offset: range.end,
+ hint_highlight_style: hint_highlights,
+ suggestion_highlight_style: suggestion_highlights,
+ highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+ active_highlights: Default::default(),
+ snapshot: self,
+ }
+ }
+
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(Default::default()..self.len(), false, None, None, None)
+ .map(|chunk| chunk.text)
+ .collect()
+ }
+
+ fn check_invariants(&self) {
+ #[cfg(any(debug_assertions, feature = "test-support"))]
+ {
+ assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+ let mut transforms = self.transforms.iter().peekable();
+ while let Some(transform) = transforms.next() {
+ let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+ if let Some(next_transform) = transforms.peek() {
+ let next_transform_is_isomorphic =
+ matches!(next_transform, Transform::Isomorphic(_));
+ assert!(
+ !transform_is_isomorphic || !next_transform_is_isomorphic,
+ "two adjacent isomorphic transforms"
+ );
+ }
+ }
+ }
+ }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+ if summary.len == 0 {
+ return;
+ }
+
+ let mut summary = Some(summary);
+ sum_tree.update_last(
+ |transform| {
+ if let Transform::Isomorphic(transform) = transform {
+ *transform += summary.take().unwrap();
+ }
+ },
+ &(),
+ );
+
+ if let Some(summary) = summary {
+ sum_tree.push(Transform::Isomorphic(summary), &());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{InlayId, MultiBuffer};
+ use gpui::AppContext;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::{cmp::Reverse, env, sync::Arc};
+ use sum_tree::TreeMap;
+ use text::Patch;
+ use util::post_inc;
+
+ #[gpui::test]
+ fn test_basic_inlays(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+ let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abcdefghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![(
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|123|",
+ },
+ )],
+ );
+ assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ // Edits before or after the inlay should not affect it.
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+ });
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+ // An edit surrounding the inlay should invalidate it.
+ buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(3),
+ text: "|123|",
+ },
+ ),
+ (
+ InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|456|",
+ },
+ ),
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+ // Edits ending where the inlay starts should not move it if it has a left bias.
+ buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+ InlayPoint::new(0, 1)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+ InlayPoint::new(0, 2)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+ InlayPoint::new(0, 8)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+ InlayPoint::new(0, 10)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+ InlayPoint::new(0, 11)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+ InlayPoint::new(0, 17)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+ InlayPoint::new(0, 18)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+ InlayPoint::new(0, 18)
+ );
+
+ // The inlays can be manually removed.
+ let (inlay_snapshot, _) = inlay_map
+ .splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
+ assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+ }
+
+ #[gpui::test]
+ fn test_inlay_buffer_rows(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(0),
+ text: "|123|\n",
+ },
+ ),
+ (
+ InlayId::Hint(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(4),
+ text: "|456|",
+ },
+ ),
+ (
+ InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ InlayProperties {
+ position: buffer.read(cx).snapshot(cx).anchor_before(7),
+ text: "\n|567|\n",
+ },
+ ),
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+ assert_eq!(
+ inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ vec![Some(0), None, Some(1), None, None, Some(2)]
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+ init_test(cx);
+
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let len = rng.gen_range(0..30);
+ let buffer = if rng.gen() {
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let mut next_inlay_id = 0;
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+
+ let mut highlights = TreeMap::default();
+ let highlight_count = rng.gen_range(0_usize..10);
+ let mut highlight_ranges = (0..highlight_count)
+ .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+ .collect::<Vec<_>>();
+ highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+ log::info!("highlighting ranges {:?}", highlight_ranges);
+ let highlight_ranges = highlight_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+ })
+ .collect::<Vec<_>>();
+
+ highlights.insert(
+ Some(TypeId::of::<()>()),
+ Arc::new((HighlightStyle::default(), highlight_ranges)),
+ );
+
+ let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ for _ in 0..operations {
+ let mut inlay_edits = Patch::default();
+
+ let mut prev_inlay_text = inlay_snapshot.text();
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=50 => {
+ let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ log::info!("mutated text: {:?}", snapshot.text());
+ inlay_edits = Patch::new(edits);
+ }
+ _ => buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ let edits = subscription.consume().into_inner();
+ log::info!("editing {:?}", edits);
+ buffer_edits.extend(edits);
+ }),
+ };
+
+ let (new_inlay_snapshot, new_inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ inlay_snapshot = new_inlay_snapshot;
+ inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+ let inlays = inlay_map
+ .inlays
+ .iter()
+ .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+ .map(|inlay| {
+ let offset = inlay.position.to_offset(&buffer_snapshot);
+ (offset, inlay.clone())
+ })
+ .collect::<Vec<_>>();
+ let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
+ for (offset, inlay) in inlays.into_iter().rev() {
+ expected_text.replace(offset..offset, &inlay.text.to_string());
+ }
+ assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+ let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+ assert_eq!(
+ expected_buffer_rows.len() as u32,
+ expected_text.max_point().row + 1
+ );
+ for row_start in 0..expected_buffer_rows.len() {
+ assert_eq!(
+ inlay_snapshot
+ .buffer_rows(row_start as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[row_start..],
+ "incorrect buffer rows starting at {}",
+ row_start
+ );
+ }
+
+ for _ in 0..5 {
+ let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+ end = expected_text.clip_offset(end, Bias::Right);
+ let mut start = rng.gen_range(0..=end);
+ start = expected_text.clip_offset(start, Bias::Right);
+
+ let actual_text = inlay_snapshot
+ .chunks(
+ InlayOffset(start)..InlayOffset(end),
+ false,
+ Some(&highlights),
+ None,
+ None,
+ )
+ .map(|chunk| chunk.text)
+ .collect::<String>();
+ assert_eq!(
+ actual_text,
+ expected_text.slice(start..end).to_string(),
+ "incorrect text in range {:?}",
+ start..end
+ );
+
+ assert_eq!(
+ inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+ expected_text.slice(start..end).summary()
+ );
+ }
+
+ for edit in inlay_edits {
+ prev_inlay_text.replace_range(
+ edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+ &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+ );
+ }
+ assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+ assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+ assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+ let mut buffer_point = Point::default();
+ let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ let mut buffer_chars = buffer_snapshot.chars_at(0);
+ loop {
+ // Ensure conversion from buffer coordinates to inlay coordinates
+ // is consistent.
+ let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+ inlay_point
+ );
+
+ // No matter which bias we clip an inlay point with, it doesn't move
+ // because it was constructed from a buffer point.
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Left),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped left",
+ buffer_point
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Right),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped right",
+ buffer_point
+ );
+
+ if let Some(ch) = buffer_chars.next() {
+ if ch == '\n' {
+ buffer_point += Point::new(1, 0);
+ } else {
+ buffer_point += Point::new(0, ch.len_utf8() as u32);
+ }
+
+ // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+ let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ assert!(new_inlay_point > inlay_point);
+ inlay_point = new_inlay_point;
+ } else {
+ break;
+ }
+ }
+
+ let mut inlay_point = InlayPoint::default();
+ let mut inlay_offset = InlayOffset::default();
+ for ch in expected_text.chars() {
+ assert_eq!(
+ inlay_snapshot.to_offset(inlay_point),
+ inlay_offset,
+ "invalid to_offset({:?})",
+ inlay_point
+ );
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_offset),
+ inlay_point,
+ "invalid to_point({:?})",
+ inlay_offset
+ );
+
+ let mut bytes = [0; 4];
+ for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+ inlay_offset.0 += 1;
+ if *byte == b'\n' {
+ inlay_point.0 += Point::new(1, 0);
+ } else {
+ inlay_point.0 += Point::new(0, 1);
+ }
+
+ let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+ let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+ assert!(
+ clipped_left_point <= clipped_right_point,
+ "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+ inlay_point,
+ clipped_left_point,
+ clipped_right_point
+ );
+
+ // Ensure the clipped points are at valid text locations.
+ assert_eq!(
+ clipped_left_point.0,
+ expected_text.clip_point(clipped_left_point.0, Bias::Left)
+ );
+ assert_eq!(
+ clipped_right_point.0,
+ expected_text.clip_point(clipped_right_point.0, Bias::Right)
+ );
+
+ // Ensure the clipped points never overshoot the end of the map.
+ assert!(clipped_left_point <= inlay_snapshot.max_point());
+ assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+ // Ensure the clipped points are at valid buffer locations.
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+ clipped_left_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_left_point,
+ inlay_snapshot.to_buffer_point(clipped_left_point),
+ );
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+ clipped_right_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_right_point,
+ inlay_snapshot.to_buffer_point(clipped_right_point),
+ );
+ }
+ }
+ }
+ }
+
+ fn init_test(cx: &mut AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+}
@@ -1,871 +0,0 @@
-use super::{
- fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
- TextHighlights,
-};
-use crate::{MultiBufferSnapshot, ToPoint};
-use gpui::fonts::HighlightStyle;
-use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
-use parking_lot::Mutex;
-use std::{
- cmp,
- ops::{Add, AddAssign, Range, Sub},
-};
-use util::post_inc;
-
-pub type SuggestionEdit = Edit<SuggestionOffset>;
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionOffset(pub usize);
-
-impl Add for SuggestionOffset {
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self::Output {
- Self(self.0 + rhs.0)
- }
-}
-
-impl Sub for SuggestionOffset {
- type Output = Self;
-
- fn sub(self, rhs: Self) -> Self::Output {
- Self(self.0 - rhs.0)
- }
-}
-
-impl AddAssign for SuggestionOffset {
- fn add_assign(&mut self, rhs: Self) {
- self.0 += rhs.0;
- }
-}
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionPoint(pub Point);
-
-impl SuggestionPoint {
- pub fn new(row: u32, column: u32) -> Self {
- Self(Point::new(row, column))
- }
-
- pub fn row(self) -> u32 {
- self.0.row
- }
-
- pub fn column(self) -> u32 {
- self.0.column
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Suggestion<T> {
- pub position: T,
- pub text: Rope,
-}
-
-pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
-
-impl SuggestionMap {
- pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
- let snapshot = SuggestionSnapshot {
- fold_snapshot,
- suggestion: None,
- version: 0,
- };
- (Self(Mutex::new(snapshot.clone())), snapshot)
- }
-
- pub fn replace<T>(
- &self,
- new_suggestion: Option<Suggestion<T>>,
- fold_snapshot: FoldSnapshot,
- fold_edits: Vec<FoldEdit>,
- ) -> (
- SuggestionSnapshot,
- Vec<SuggestionEdit>,
- Option<Suggestion<FoldOffset>>,
- )
- where
- T: ToPoint,
- {
- let new_suggestion = new_suggestion.map(|new_suggestion| {
- let buffer_point = new_suggestion
- .position
- .to_point(fold_snapshot.buffer_snapshot());
- let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
- let fold_offset = fold_point.to_offset(&fold_snapshot);
- Suggestion {
- position: fold_offset,
- text: new_suggestion.text,
- }
- });
-
- let (_, edits) = self.sync(fold_snapshot, fold_edits);
- let mut snapshot = self.0.lock();
-
- let mut patch = Patch::new(edits);
- let old_suggestion = snapshot.suggestion.take();
- if let Some(suggestion) = &old_suggestion {
- patch = patch.compose([SuggestionEdit {
- old: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
- new: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0),
- }]);
- }
-
- if let Some(suggestion) = new_suggestion.as_ref() {
- patch = patch.compose([SuggestionEdit {
- old: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0),
- new: SuggestionOffset(suggestion.position.0)
- ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
- }]);
- }
-
- snapshot.suggestion = new_suggestion;
- snapshot.version += 1;
- (snapshot.clone(), patch.into_inner(), old_suggestion)
- }
-
- pub fn sync(
- &self,
- fold_snapshot: FoldSnapshot,
- fold_edits: Vec<FoldEdit>,
- ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
- let mut snapshot = self.0.lock();
-
- if snapshot.fold_snapshot.version != fold_snapshot.version {
- snapshot.version += 1;
- }
-
- let mut suggestion_edits = Vec::new();
-
- let mut suggestion_old_len = 0;
- let mut suggestion_new_len = 0;
- for fold_edit in fold_edits {
- let start = fold_edit.new.start;
- let end = FoldOffset(start.0 + fold_edit.old_len().0);
- if let Some(suggestion) = snapshot.suggestion.as_mut() {
- if end <= suggestion.position {
- suggestion.position.0 += fold_edit.new_len().0;
- suggestion.position.0 -= fold_edit.old_len().0;
- } else if start > suggestion.position {
- suggestion_old_len = suggestion.text.len();
- suggestion_new_len = suggestion_old_len;
- } else {
- suggestion_old_len = suggestion.text.len();
- snapshot.suggestion.take();
- suggestion_edits.push(SuggestionEdit {
- old: SuggestionOffset(fold_edit.old.start.0)
- ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
- new: SuggestionOffset(fold_edit.new.start.0)
- ..SuggestionOffset(fold_edit.new.end.0),
- });
- continue;
- }
- }
-
- suggestion_edits.push(SuggestionEdit {
- old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
- ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
- new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
- ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
- });
- }
- snapshot.fold_snapshot = fold_snapshot;
-
- (snapshot.clone(), suggestion_edits)
- }
-
- pub fn has_suggestion(&self) -> bool {
- let snapshot = self.0.lock();
- snapshot.suggestion.is_some()
- }
-}
-
-#[derive(Clone)]
-pub struct SuggestionSnapshot {
- pub fold_snapshot: FoldSnapshot,
- pub suggestion: Option<Suggestion<FoldOffset>>,
- pub version: usize,
-}
-
-impl SuggestionSnapshot {
- pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- self.fold_snapshot.buffer_snapshot()
- }
-
- pub fn max_point(&self) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
- let mut max_point = suggestion_point.0;
- max_point += suggestion.text.max_point();
- max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
- SuggestionPoint(max_point)
- } else {
- SuggestionPoint(self.fold_snapshot.max_point().0)
- }
- }
-
- pub fn len(&self) -> SuggestionOffset {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let mut len = suggestion.position.0;
- len += suggestion.text.len();
- len += self.fold_snapshot.len().0 - suggestion.position.0;
- SuggestionOffset(len)
- } else {
- SuggestionOffset(self.fold_snapshot.len().0)
- }
- }
-
- pub fn line_len(&self, row: u32) -> u32 {
- if let Some(suggestion) = &self.suggestion {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if row < suggestion_start.row {
- self.fold_snapshot.line_len(row)
- } else if row > suggestion_end.row {
- self.fold_snapshot
- .line_len(suggestion_start.row + (row - suggestion_end.row))
- } else {
- let mut result = suggestion.text.line_len(row - suggestion_start.row);
- if row == suggestion_start.row {
- result += suggestion_start.column;
- }
- if row == suggestion_end.row {
- result +=
- self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
- }
- result
- }
- } else {
- self.fold_snapshot.line_len(row)
- }
- }
-
- pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- if point.0 <= suggestion_start {
- SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
- } else if point.0 > suggestion_end {
- let fold_point = self.fold_snapshot.clip_point(
- FoldPoint(suggestion_start + (point.0 - suggestion_end)),
- bias,
- );
- let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
- if bias == Bias::Left && suggestion_point == suggestion_end {
- SuggestionPoint(suggestion_start)
- } else {
- SuggestionPoint(suggestion_point)
- }
- } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
- SuggestionPoint(suggestion_start)
- } else {
- let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
- > suggestion_start.column
- {
- FoldPoint(suggestion_start + Point::new(0, 1))
- } else {
- FoldPoint(suggestion_start + Point::new(1, 0))
- };
- let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
- SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
- }
- } else {
- SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
- }
- }
-
- pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if point.0 <= suggestion_start {
- SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
- } else if point.0 > suggestion_end {
- let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
- .to_offset(&self.fold_snapshot);
- SuggestionOffset(fold_offset.0 + suggestion.text.len())
- } else {
- let offset_in_suggestion =
- suggestion.text.point_to_offset(point.0 - suggestion_start);
- SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
- }
- } else {
- SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
- }
- }
-
- pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
- if offset.0 <= suggestion.position.0 {
- SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
- } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
- let fold_point = FoldOffset(offset.0 - suggestion.text.len())
- .to_point(&self.fold_snapshot)
- .0;
-
- SuggestionPoint(
- suggestion_point_start
- + suggestion.text.max_point()
- + (fold_point - suggestion_point_start),
- )
- } else {
- let point_in_suggestion = suggestion
- .text
- .offset_to_point(offset.0 - suggestion.position.0);
- SuggestionPoint(suggestion_point_start + point_in_suggestion)
- }
- } else {
- SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
- }
- }
-
- pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
-
- if point.0 <= suggestion_start {
- FoldPoint(point.0)
- } else if point.0 > suggestion_end {
- FoldPoint(suggestion_start + (point.0 - suggestion_end))
- } else {
- FoldPoint(suggestion_start)
- }
- } else {
- FoldPoint(point.0)
- }
- }
-
- pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-
- if point.0 <= suggestion_start {
- SuggestionPoint(point.0)
- } else {
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
- }
- } else {
- SuggestionPoint(point.0)
- }
- }
-
- pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- let mut summary = TextSummary::default();
-
- let prefix_range =
- cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
- if prefix_range.start < prefix_range.end {
- summary += self.fold_snapshot.text_summary_for_range(
- FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
- );
- }
-
- let suggestion_range =
- cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
- if suggestion_range.start < suggestion_range.end {
- let point_range = suggestion_range.start - suggestion_start
- ..suggestion_range.end - suggestion_start;
- let offset_range = suggestion.text.point_to_offset(point_range.start)
- ..suggestion.text.point_to_offset(point_range.end);
- summary += suggestion
- .text
- .cursor(offset_range.start)
- .summary::<TextSummary>(offset_range.end);
- }
-
- let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
- if suffix_range.start < suffix_range.end {
- let start = suggestion_start + (suffix_range.start - suggestion_end);
- let end = suggestion_start + (suffix_range.end - suggestion_end);
- summary += self
- .fold_snapshot
- .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
- }
-
- summary
- } else {
- self.fold_snapshot
- .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
- }
- }
-
- pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
- let start = self.to_offset(start);
- self.chunks(start..self.len(), false, None, None)
- .flat_map(|chunk| chunk.text.chars())
- }
-
- pub fn chunks<'a>(
- &'a self,
- range: Range<SuggestionOffset>,
- language_aware: bool,
- text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
- ) -> SuggestionChunks<'a> {
- if let Some(suggestion) = self.suggestion.as_ref() {
- let suggestion_range =
- suggestion.position.0..suggestion.position.0 + suggestion.text.len();
-
- let prefix_chunks = if range.start.0 < suggestion_range.start {
- Some(self.fold_snapshot.chunks(
- FoldOffset(range.start.0)
- ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
- language_aware,
- text_highlights,
- ))
- } else {
- None
- };
-
- let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
- ..cmp::min(range.end.0, suggestion_range.end);
- let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
- {
- let start = clipped_suggestion_range.start - suggestion_range.start;
- let end = clipped_suggestion_range.end - suggestion_range.start;
- Some(suggestion.text.chunks_in_range(start..end))
- } else {
- None
- };
-
- let suffix_chunks = if range.end.0 > suggestion_range.end {
- let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
- let end = range.end.0 - suggestion_range.len();
- Some(self.fold_snapshot.chunks(
- FoldOffset(start)..FoldOffset(end),
- language_aware,
- text_highlights,
- ))
- } else {
- None
- };
-
- SuggestionChunks {
- prefix_chunks,
- suggestion_chunks,
- suffix_chunks,
- highlight_style: suggestion_highlight,
- }
- } else {
- SuggestionChunks {
- prefix_chunks: Some(self.fold_snapshot.chunks(
- FoldOffset(range.start.0)..FoldOffset(range.end.0),
- language_aware,
- text_highlights,
- )),
- suggestion_chunks: None,
- suffix_chunks: None,
- highlight_style: None,
- }
- }
- }
-
- pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
- let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
- let start = suggestion.position.to_point(&self.fold_snapshot).0;
- let end = start + suggestion.text.max_point();
- start.row..end.row
- } else {
- u32::MAX..u32::MAX
- };
-
- let fold_buffer_rows = if row <= suggestion_range.start {
- self.fold_snapshot.buffer_rows(row)
- } else if row > suggestion_range.end {
- self.fold_snapshot
- .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
- } else {
- let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
- rows.next();
- rows
- };
-
- SuggestionBufferRows {
- current_row: row,
- suggestion_row_start: suggestion_range.start,
- suggestion_row_end: suggestion_range.end,
- fold_buffer_rows,
- }
- }
-
- #[cfg(test)]
- pub fn text(&self) -> String {
- self.chunks(Default::default()..self.len(), false, None, None)
- .map(|chunk| chunk.text)
- .collect()
- }
-}
-
-pub struct SuggestionChunks<'a> {
- prefix_chunks: Option<FoldChunks<'a>>,
- suggestion_chunks: Option<text::Chunks<'a>>,
- suffix_chunks: Option<FoldChunks<'a>>,
- highlight_style: Option<HighlightStyle>,
-}
-
-impl<'a> Iterator for SuggestionChunks<'a> {
- type Item = Chunk<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if let Some(chunks) = self.prefix_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(chunk);
- } else {
- self.prefix_chunks = None;
- }
- }
-
- if let Some(chunks) = self.suggestion_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(Chunk {
- text: chunk,
- highlight_style: self.highlight_style,
- ..Default::default()
- });
- } else {
- self.suggestion_chunks = None;
- }
- }
-
- if let Some(chunks) = self.suffix_chunks.as_mut() {
- if let Some(chunk) = chunks.next() {
- return Some(chunk);
- } else {
- self.suffix_chunks = None;
- }
- }
-
- None
- }
-}
-
-#[derive(Clone)]
-pub struct SuggestionBufferRows<'a> {
- current_row: u32,
- suggestion_row_start: u32,
- suggestion_row_end: u32,
- fold_buffer_rows: FoldBufferRows<'a>,
-}
-
-impl<'a> Iterator for SuggestionBufferRows<'a> {
- type Item = Option<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let row = post_inc(&mut self.current_row);
- if row <= self.suggestion_row_start || row > self.suggestion_row_end {
- self.fold_buffer_rows.next()
- } else {
- Some(None)
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{display_map::fold_map::FoldMap, MultiBuffer};
- use gpui::AppContext;
- use rand::{prelude::StdRng, Rng};
- use settings::SettingsStore;
- use std::{
- env,
- ops::{Bound, RangeBounds},
- };
-
- #[gpui::test]
- fn test_basic(cx: &mut AppContext) {
- let buffer = MultiBuffer::build_simple("abcdefghi", cx);
- let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
- let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
- assert_eq!(suggestion_snapshot.text(), "abcdefghi");
-
- let (suggestion_snapshot, _, _) = suggestion_map.replace(
- Some(Suggestion {
- position: 3,
- text: "123\n456".into(),
- }),
- fold_snapshot,
- Default::default(),
- );
- assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
-
- buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
- None,
- cx,
- )
- });
- let (fold_snapshot, fold_edits) = fold_map.read(
- buffer.read(cx).snapshot(cx),
- buffer_edits.consume().into_inner(),
- );
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
-
- let (mut fold_map_writer, _, _) =
- fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
- let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
- assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
-
- let (mut fold_map_writer, _, _) =
- fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
- let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
- let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
- assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
- }
-
- #[gpui::test(iterations = 100)]
- fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
- init_test(cx);
-
- let operations = env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(10);
-
- let len = rng.gen_range(0..30);
- let buffer = if rng.gen() {
- let text = util::RandomCharIter::new(&mut rng)
- .take(len)
- .collect::<String>();
- MultiBuffer::build_simple(&text, cx)
- } else {
- MultiBuffer::build_random(&mut rng, cx)
- };
- let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
- log::info!("buffer text: {:?}", buffer_snapshot.text());
-
- let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-
- for _ in 0..operations {
- let mut suggestion_edits = Patch::default();
-
- let mut prev_suggestion_text = suggestion_snapshot.text();
- let mut buffer_edits = Vec::new();
- match rng.gen_range(0..=100) {
- 0..=29 => {
- let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
- suggestion_edits = suggestion_edits.compose(edits);
- }
- 30..=59 => {
- for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
- fold_snapshot = new_fold_snapshot;
- let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- suggestion_edits = suggestion_edits.compose(edits);
- }
- }
- _ => buffer.update(cx, |buffer, cx| {
- let subscription = buffer.subscribe();
- let edit_count = rng.gen_range(1..=5);
- buffer.randomly_mutate(&mut rng, edit_count, cx);
- buffer_snapshot = buffer.snapshot(cx);
- let edits = subscription.consume().into_inner();
- log::info!("editing {:?}", edits);
- buffer_edits.extend(edits);
- }),
- };
-
- let (new_fold_snapshot, fold_edits) =
- fold_map.read(buffer_snapshot.clone(), buffer_edits);
- fold_snapshot = new_fold_snapshot;
- let (new_suggestion_snapshot, edits) =
- suggestion_map.sync(fold_snapshot.clone(), fold_edits);
- suggestion_snapshot = new_suggestion_snapshot;
- suggestion_edits = suggestion_edits.compose(edits);
-
- log::info!("buffer text: {:?}", buffer_snapshot.text());
- log::info!("folds text: {:?}", fold_snapshot.text());
- log::info!("suggestions text: {:?}", suggestion_snapshot.text());
-
- let mut expected_text = Rope::from(fold_snapshot.text().as_str());
- let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
- if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
- expected_text.replace(
- suggestion.position.0..suggestion.position.0,
- &suggestion.text.to_string(),
- );
- let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- expected_buffer_rows.splice(
- (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
- (0..suggestion_end.row - suggestion_start.row).map(|_| None),
- );
- }
- assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
- for row_start in 0..expected_buffer_rows.len() {
- assert_eq!(
- suggestion_snapshot
- .buffer_rows(row_start as u32)
- .collect::<Vec<_>>(),
- &expected_buffer_rows[row_start..],
- "incorrect buffer rows starting at {}",
- row_start
- );
- }
-
- for _ in 0..5 {
- let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
- end = expected_text.clip_offset(end, Bias::Right);
- let mut start = rng.gen_range(0..=end);
- start = expected_text.clip_offset(start, Bias::Right);
-
- let actual_text = suggestion_snapshot
- .chunks(
- SuggestionOffset(start)..SuggestionOffset(end),
- false,
- None,
- None,
- )
- .map(|chunk| chunk.text)
- .collect::<String>();
- assert_eq!(
- actual_text,
- expected_text.slice(start..end).to_string(),
- "incorrect text in range {:?}",
- start..end
- );
-
- let start_point = SuggestionPoint(expected_text.offset_to_point(start));
- let end_point = SuggestionPoint(expected_text.offset_to_point(end));
- assert_eq!(
- suggestion_snapshot.text_summary_for_range(start_point..end_point),
- expected_text.slice(start..end).summary()
- );
- }
-
- for edit in suggestion_edits.into_inner() {
- prev_suggestion_text.replace_range(
- edit.new.start.0..edit.new.start.0 + edit.old_len().0,
- &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
- );
- }
- assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
-
- assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
- assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
-
- let mut suggestion_point = SuggestionPoint::default();
- let mut suggestion_offset = SuggestionOffset::default();
- for ch in expected_text.chars() {
- assert_eq!(
- suggestion_snapshot.to_offset(suggestion_point),
- suggestion_offset,
- "invalid to_offset({:?})",
- suggestion_point
- );
- assert_eq!(
- suggestion_snapshot.to_point(suggestion_offset),
- suggestion_point,
- "invalid to_point({:?})",
- suggestion_offset
- );
- assert_eq!(
- suggestion_snapshot
- .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
- suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
- );
-
- let mut bytes = [0; 4];
- for byte in ch.encode_utf8(&mut bytes).as_bytes() {
- suggestion_offset.0 += 1;
- if *byte == b'\n' {
- suggestion_point.0 += Point::new(1, 0);
- } else {
- suggestion_point.0 += Point::new(0, 1);
- }
-
- let clipped_left_point =
- suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
- let clipped_right_point =
- suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
- assert!(
- clipped_left_point <= clipped_right_point,
- "clipped left point {:?} is greater than clipped right point {:?}",
- clipped_left_point,
- clipped_right_point
- );
- assert_eq!(
- clipped_left_point.0,
- expected_text.clip_point(clipped_left_point.0, Bias::Left)
- );
- assert_eq!(
- clipped_right_point.0,
- expected_text.clip_point(clipped_right_point.0, Bias::Right)
- );
- assert!(clipped_left_point <= suggestion_snapshot.max_point());
- assert!(clipped_right_point <= suggestion_snapshot.max_point());
-
- if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
- let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
- let suggestion_end = suggestion_start + suggestion.text.max_point();
- let invalid_range = (
- Bound::Excluded(suggestion_start),
- Bound::Included(suggestion_end),
- );
- assert!(
- !invalid_range.contains(&clipped_left_point.0),
- "clipped left point {:?} is inside invalid suggestion range {:?}",
- clipped_left_point,
- invalid_range
- );
- assert!(
- !invalid_range.contains(&clipped_right_point.0),
- "clipped right point {:?} is inside invalid suggestion range {:?}",
- clipped_right_point,
- invalid_range
- );
- }
- }
- }
- }
- }
-
- fn init_test(cx: &mut AppContext) {
- cx.set_global(SettingsStore::test(cx));
- theme::init((), cx);
- }
-
- impl SuggestionMap {
- pub fn randomly_mutate(
- &self,
- rng: &mut impl Rng,
- ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
- let fold_snapshot = self.0.lock().fold_snapshot.clone();
- let new_suggestion = if rng.gen_bool(0.3) {
- None
- } else {
- let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
- let len = rng.gen_range(0..30);
- Some(Suggestion {
- position: index,
- text: util::RandomCharIter::new(rng)
- .take(len)
- .filter(|ch| *ch != '\r')
- .collect::<String>()
- .as_str()
- .into(),
- })
- };
-
- log::info!("replacing suggestion with {:?}", new_suggestion);
- let (snapshot, edits, _) =
- self.replace(new_suggestion, fold_snapshot, Default::default());
- (snapshot, edits)
- }
- }
-}
@@ -1,80 +1,76 @@
use super::{
- suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
+ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
TextHighlights,
};
use crate::MultiBufferSnapshot;
use gpui::fonts::HighlightStyle;
use language::{Chunk, Point};
-use parking_lot::Mutex;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
-pub struct TabMap(Mutex<TabSnapshot>);
+pub struct TabMap(TabSnapshot);
impl TabMap {
- pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+ pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
- suggestion_snapshot: input,
+ fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
version: 0,
};
- (Self(Mutex::new(snapshot.clone())), snapshot)
+ (Self(snapshot.clone()), snapshot)
}
#[cfg(test)]
- pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
- self.0.lock().max_expansion_column = column;
- self.0.lock().clone()
+ pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+ self.0.max_expansion_column = column;
+ self.0.clone()
}
pub fn sync(
- &self,
- suggestion_snapshot: SuggestionSnapshot,
- mut suggestion_edits: Vec<SuggestionEdit>,
+ &mut self,
+ fold_snapshot: FoldSnapshot,
+ mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (TabSnapshot, Vec<TabEdit>) {
- let mut old_snapshot = self.0.lock();
+ let old_snapshot = &mut self.0;
let mut new_snapshot = TabSnapshot {
- suggestion_snapshot,
+ fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
version: old_snapshot.version,
};
- if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
+ if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
new_snapshot.version += 1;
}
- let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
+ let mut tab_edits = Vec::with_capacity(fold_edits.len());
if old_snapshot.tab_size == new_snapshot.tab_size {
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
- for suggestion_edit in &mut suggestion_edits {
- let old_end = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.end);
- let old_end_row_successor_offset =
- old_snapshot.suggestion_snapshot.to_offset(cmp::min(
- SuggestionPoint::new(old_end.row() + 1, 0),
- old_snapshot.suggestion_snapshot.max_point(),
- ));
- let new_end = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.end);
+ for fold_edit in &mut fold_edits {
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let old_end_row_successor_offset = cmp::min(
+ FoldPoint::new(old_end.row() + 1, 0),
+ old_snapshot.fold_snapshot.max_point(),
+ )
+ .to_offset(&old_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
let mut offset_from_edit = 0;
let mut first_tab_offset = None;
let mut last_tab_with_changed_expansion_offset = None;
- 'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
- suggestion_edit.old.end..old_end_row_successor_offset,
+ 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+ fold_edit.old.end..old_end_row_successor_offset,
false,
None,
None,
+ None,
) {
for (ix, _) in chunk.text.match_indices('\t') {
let offset_from_edit = offset_from_edit + (ix as u32);
@@ -102,39 +98,31 @@ impl TabMap {
}
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
- suggestion_edit.old.end.0 += offset as usize + 1;
- suggestion_edit.new.end.0 += offset as usize + 1;
+ fold_edit.old.end.0 += offset as usize + 1;
+ fold_edit.new.end.0 += offset as usize + 1;
}
}
// Combine any edits that overlap due to the expansion.
let mut ix = 1;
- while ix < suggestion_edits.len() {
- let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
+ while ix < fold_edits.len() {
+ let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
let prev_edit = prev_edits.last_mut().unwrap();
let edit = &next_edits[0];
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = edit.old.end;
prev_edit.new.end = edit.new.end;
- suggestion_edits.remove(ix);
+ fold_edits.remove(ix);
} else {
ix += 1;
}
}
- for suggestion_edit in suggestion_edits {
- let old_start = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.start);
- let old_end = old_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.old.end);
- let new_start = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.start);
- let new_end = new_snapshot
- .suggestion_snapshot
- .to_point(suggestion_edit.new.end);
+ for fold_edit in fold_edits {
+ let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -155,7 +143,7 @@ impl TabMap {
#[derive(Clone)]
pub struct TabSnapshot {
- pub suggestion_snapshot: SuggestionSnapshot,
+ pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
@@ -163,18 +151,15 @@ pub struct TabSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
- self.suggestion_snapshot.buffer_snapshot()
+ &self.fold_snapshot.inlay_snapshot.buffer
}
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
- self.to_tab_point(SuggestionPoint::new(
- row,
- self.suggestion_snapshot.line_len(row),
- ))
- .0
- .column
+ self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+ .0
+ .column
} else {
max_point.column()
}
@@ -185,10 +170,10 @@ impl TabSnapshot {
}
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
- let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
- let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
+ let input_start = self.to_fold_point(range.start, Bias::Left).0;
+ let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
- .suggestion_snapshot
+ .fold_snapshot
.text_summary_for_range(input_start..input_end);
let mut first_line_chars = 0;
@@ -198,7 +183,7 @@ impl TabSnapshot {
self.max_point()
};
for c in self
- .chunks(range.start..line_end, false, None, None)
+ .chunks(range.start..line_end, false, None, None, None)
.flat_map(|chunk| chunk.text.chars())
{
if c == '\n' {
@@ -217,6 +202,7 @@ impl TabSnapshot {
false,
None,
None,
+ None,
)
.flat_map(|chunk| chunk.text.chars())
{
@@ -238,15 +224,17 @@ impl TabSnapshot {
range: Range<TabPoint>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
- self.to_suggestion_point(range.start, Bias::Left);
+ self.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
- let input_start = self.suggestion_snapshot.to_offset(input_start);
+ let input_start = input_start.to_offset(&self.fold_snapshot);
let input_end = self
- .suggestion_snapshot
- .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
+ .to_fold_point(range.end, Bias::Right)
+ .0
+ .to_offset(&self.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
@@ -254,11 +242,12 @@ impl TabSnapshot {
};
TabChunks {
- suggestion_chunks: self.suggestion_snapshot.chunks(
+ fold_chunks: self.fold_snapshot.chunks(
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_column,
column: expanded_char_column,
@@ -275,63 +264,58 @@ impl TabSnapshot {
}
}
- pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
- self.suggestion_snapshot.buffer_rows(row)
+ pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+ self.fold_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
- self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
+ self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
.map(|chunk| chunk.text)
.collect()
}
pub fn max_point(&self) -> TabPoint {
- self.to_tab_point(self.suggestion_snapshot.max_point())
+ self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
- self.suggestion_snapshot
- .clip_point(self.to_suggestion_point(point, bias).0, bias),
+ self.fold_snapshot
+ .clip_point(self.to_fold_point(point, bias).0, bias),
)
}
- pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
- let chars = self
- .suggestion_snapshot
- .chars_at(SuggestionPoint::new(input.row(), 0));
+ pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
TabPoint::new(input.row(), expanded)
}
- pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
- let chars = self
- .suggestion_snapshot
- .chars_at(SuggestionPoint::new(output.row(), 0));
+ pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
self.collapse_tabs(chars, expanded, bias);
(
- SuggestionPoint::new(output.row(), collapsed as u32),
+ FoldPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
- let fold_point = self
- .suggestion_snapshot
- .fold_snapshot
- .to_fold_point(point, bias);
- let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
- self.to_tab_point(suggestion_point)
+ let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+ let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+ self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
- let suggestion_point = self.to_suggestion_point(point, bias).0;
- let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
- fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
+ let fold_point = self.to_fold_point(point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ self.fold_snapshot
+ .inlay_snapshot
+ .to_buffer_point(inlay_point)
}
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
@@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
- suggestion_chunks: SuggestionChunks<'a>,
+ fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
max_expansion_column: u32,
@@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> {
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
- if let Some(chunk) = self.suggestion_chunks.next() {
+ if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
@@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> {
mod tests {
use super::*;
use crate::{
- display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
@@ -583,9 +567,9 @@ mod tests {
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
@@ -600,9 +584,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
@@ -615,6 +599,7 @@ mod tests {
false,
None,
None,
+ None,
)
.map(|c| c.text)
.collect::<String>(),
@@ -626,16 +611,16 @@ mod tests {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
- tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+ tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
tab_snapshot
- .to_suggestion_point(TabPoint(output_point), Bias::Left)
+ .to_fold_point(TabPoint(output_point), Bias::Left)
.0,
- SuggestionPoint(input_point),
- "to_suggestion_point({output_point:?})"
+ FoldPoint(input_point),
+ "to_fold_point({output_point:?})"
);
}
}
@@ -648,9 +633,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
@@ -662,9 +647,9 @@ mod tests {
let buffer = MultiBuffer::build_simple(&input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
- let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
- let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
@@ -689,7 +674,7 @@ mod tests {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
- for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
+ for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
@@ -721,15 +706,16 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
- let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
+ let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
- let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+ let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
- let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
@@ -757,7 +743,7 @@ mod tests {
let expected_summary = TextSummary::from(expected_text.as_str());
assert_eq!(
tabs_snapshot
- .chunks(start..end, false, None, None)
+ .chunks(start..end, false, None, None, None)
.map(|c| c.text)
.collect::<String>(),
expected_text,
@@ -767,7 +753,7 @@ mod tests {
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
- if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
+ if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
@@ -1,5 +1,5 @@
use super::{
- suggestion_map::SuggestionBufferRows,
+ fold_map::FoldBufferRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
TextHighlights,
};
@@ -65,7 +65,7 @@ pub struct WrapChunks<'a> {
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
- input_buffer_rows: SuggestionBufferRows<'a>,
+ input_buffer_rows: FoldBufferRows<'a>,
input_buffer_row: Option<u32>,
output_row: u32,
soft_wrapped: bool,
@@ -446,6 +446,7 @@ impl WrapSnapshot {
false,
None,
None,
+ None,
);
let mut edit_transforms = Vec::<Transform>::new();
for _ in edit.new_rows.start..edit.new_rows.end {
@@ -575,7 +576,8 @@ impl WrapSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- suggestion_highlight: Option<HighlightStyle>,
+ hint_highlights: Option<HighlightStyle>,
+ suggestion_highlights: Option<HighlightStyle>,
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
@@ -593,7 +595,8 @@ impl WrapSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- suggestion_highlight,
+ hint_highlights,
+ suggestion_highlights,
),
input_chunk: Default::default(),
output_position: output_start,
@@ -757,28 +760,18 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
- let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
+ let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut expected_buffer_rows = Vec::new();
- let mut prev_fold_row = 0;
+ let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
- let suggestion_point = self
- .tab_snapshot
- .to_suggestion_point(tab_point, Bias::Left)
- .0;
- let fold_point = self
- .tab_snapshot
- .suggestion_snapshot
- .to_fold_point(suggestion_point);
- if fold_point.row() == prev_fold_row && display_row != 0 {
+ if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
- let buffer_point = fold_point
- .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
- expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
- prev_fold_row = fold_point.row();
+ expected_buffer_rows.push(input_buffer_rows.next().unwrap());
}
+ prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
@@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
mod tests {
use super::*;
use crate::{
- display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::test::observe;
@@ -1089,11 +1082,11 @@ mod tests {
});
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
- let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
@@ -1122,6 +1115,7 @@ mod tests {
);
log::info!("Wrapped text: {:?}", actual_text);
+ let mut next_inlay_id = 0;
let mut edits = Vec::new();
for _i in 0..operations {
log::info!("{} ==============================================", _i);
@@ -1139,10 +1133,8 @@ mod tests {
}
20..=39 => {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1151,10 +1143,11 @@ mod tests {
}
}
40..=59 => {
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.randomly_mutate(&mut rng);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1173,13 +1166,12 @@ mod tests {
}
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
- let (suggestion_snapshot, suggestion_edits) =
- suggestion_map.sync(fold_snapshot, fold_edits);
- log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
- let (tabs_snapshot, tab_edits) =
- tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+ let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
@@ -1227,7 +1219,7 @@ mod tests {
if tab_size.get() == 1
|| !wrapped_snapshot
.tab_snapshot
- .suggestion_snapshot
+ .fold_snapshot
.text()
.contains('\t')
{
@@ -1328,8 +1320,14 @@ mod tests {
}
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
- self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
- .map(|h| h.text)
+ self.chunks(
+ wrap_row..self.max_point().row() + 1,
+ false,
+ None,
+ None,
+ None,
+ )
+ .map(|h| h.text)
}
fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -1352,7 +1350,7 @@ mod tests {
}
let actual_text = self
- .chunks(start_row..end_row, true, None, None)
+ .chunks(start_row..end_row, true, None, None, None)
.map(|c| c.text)
.collect::<String>();
assert_eq!(
@@ -2,6 +2,7 @@ mod blink_manager;
pub mod display_map;
mod editor_settings;
mod element;
+mod inlay_hint_cache;
mod git;
mod highlight_matching_bracket;
@@ -52,11 +53,12 @@ use gpui::{
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
use language::{
- language_settings::{self, all_language_settings},
+ language_settings::{self, all_language_settings, InlayHintSettings},
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -64,11 +66,12 @@ use language::{
use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
};
+use log::error;
+use multi_buffer::ToOffsetUtf16;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
};
-use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
use ordered_float::OrderedFloat;
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
use scroll::{
@@ -85,12 +88,13 @@ use std::{
cmp::{self, Ordering, Reverse},
mem,
num::NonZeroU32,
- ops::{Deref, DerefMut, Range},
+ ops::{ControlFlow, Deref, DerefMut, Range},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
pub use sum_tree::Bias;
+use text::Rope;
use theme::{DiagnosticStyle, Theme, ThemeSettings};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace};
@@ -180,6 +184,12 @@ pub struct GutterHover {
pub hovered: bool,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+ Suggestion(usize),
+ Hint(usize),
+}
+
actions!(
editor,
[
@@ -535,6 +545,8 @@ pub struct Editor {
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
copilot_state: CopilotState,
+ inlay_hint_cache: InlayHintCache,
+ next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
}
@@ -1056,6 +1068,7 @@ pub struct CopilotState {
cycled: bool,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
+ suggestion: Option<Inlay>,
}
impl Default for CopilotState {
@@ -1067,6 +1080,7 @@ impl Default for CopilotState {
completions: Default::default(),
active_completion_index: 0,
cycled: false,
+ suggestion: None,
}
}
}
@@ -1181,6 +1195,14 @@ enum GotoDefinitionKind {
Type,
}
+#[derive(Debug, Copy, Clone)]
+enum InlayRefreshReason {
+ SettingsChange(InlayHintSettings),
+ NewLinesShown,
+ ExcerptEdited,
+ RefreshRequested,
+}
+
impl Editor {
pub fn single_line(
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1282,15 +1304,28 @@ impl Editor {
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
- let mut project_subscription = None;
- if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
+ let mut project_subscriptions = Vec::new();
+ if mode == EditorMode::Full {
if let Some(project) = project.as_ref() {
- project_subscription = Some(cx.observe(project, |_, _, cx| {
- cx.emit(Event::TitleChanged);
- }))
+ if buffer.read(cx).is_singleton() {
+ project_subscriptions.push(cx.observe(project, |_, _, cx| {
+ cx.emit(Event::TitleChanged);
+ }));
+ }
+ project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+ if let project::Event::RefreshInlays = event {
+ editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+ };
+ }));
}
}
+ let inlay_hint_settings = inlay_hint_settings(
+ selections.newest_anchor().head(),
+ &buffer.read(cx).snapshot(cx),
+ cx,
+ );
+
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1324,6 +1359,7 @@ impl Editor {
.add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
completion_tasks: Default::default(),
next_completion_id: 0,
+ next_inlay_id: 0,
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
@@ -1340,6 +1376,7 @@ impl Editor {
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
copilot_state: Default::default(),
+ inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
@@ -1350,9 +1387,7 @@ impl Editor {
],
};
- if let Some(project_subscription) = project_subscription {
- this._subscriptions.push(project_subscription);
- }
+ this._subscriptions.extend(project_subscriptions);
this.end_selection(cx);
this.scroll_manager.show_scrollbar(cx);
@@ -1873,7 +1908,7 @@ impl Editor {
s.set_pending(pending, mode);
});
} else {
- log::error!("update_selection dispatched with no pending selection");
+ error!("update_selection dispatched with no pending selection");
return;
}
@@ -2577,6 +2612,106 @@ impl Editor {
}
}
+ fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+ if self.project.is_none() || self.mode != EditorMode::Full {
+ return;
+ }
+
+ let invalidate_cache = match reason {
+ InlayRefreshReason::SettingsChange(new_settings) => {
+ match self.inlay_hint_cache.update_settings(
+ &self.buffer,
+ new_settings,
+ self.visible_inlay_hints(cx),
+ cx,
+ ) {
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })) => {
+ self.splice_inlay_hints(to_remove, to_insert, cx);
+ return;
+ }
+ ControlFlow::Break(None) => return,
+ ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
+ }
+ }
+ InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
+ InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
+ InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
+ };
+
+ self.inlay_hint_cache.refresh_inlay_hints(
+ self.excerpt_visible_offsets(cx),
+ invalidate_cache,
+ cx,
+ )
+ }
+
+ fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+ self.display_map
+ .read(cx)
+ .current_inlays()
+ .filter(move |inlay| {
+ Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+ })
+ .cloned()
+ .collect()
+ }
+
+ fn excerpt_visible_offsets(
+ &self,
+ cx: &mut ViewContext<'_, '_, Editor>,
+ ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
+ let multi_buffer = self.buffer().read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ let multi_buffer_visible_start = self
+ .scroll_manager
+ .anchor()
+ .anchor
+ .to_point(&multi_buffer_snapshot);
+ let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+ multi_buffer_visible_start
+ + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
+ Bias::Left,
+ );
+ let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
+ multi_buffer
+ .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+ .into_iter()
+ .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+ .map(|(buffer, excerpt_visible_range, excerpt_id)| {
+ (excerpt_id, (buffer, excerpt_visible_range))
+ })
+ .collect()
+ }
+
+ fn splice_inlay_hints(
+ &self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let buffer = self.buffer.read(cx).read(cx);
+ let new_inlays = to_insert
+ .into_iter()
+ .map(|(position, id, hint)| {
+ let mut text = hint.text();
+ if hint.padding_right {
+ text.push(' ');
+ }
+ if hint.padding_left {
+ text.insert(0, ' ');
+ }
+ (id, InlayProperties { position, text })
+ })
+ .collect();
+ drop(buffer);
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.splice_inlays(to_remove, new_inlays, cx);
+ });
+ }
+
fn trigger_on_type_formatting(
&self,
input: String,
@@ -3227,10 +3362,7 @@ impl Editor {
}
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(suggestion) = self
- .display_map
- .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
- {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
if let Some((copilot, completion)) =
Copilot::global(cx).zip(self.copilot_state.active_completion())
{
@@ -3249,7 +3381,7 @@ impl Editor {
}
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if self.has_active_copilot_suggestion(cx) {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
@@ -3260,8 +3392,9 @@ impl Editor {
self.report_copilot_event(None, false, cx)
}
- self.display_map
- .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
+ });
cx.notify();
true
} else {
@@ -3282,7 +3415,26 @@ impl Editor {
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
- self.display_map.read(cx).has_suggestion()
+ if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+ let buffer = self.buffer.read(cx).read(cx);
+ suggestion.position.is_valid(&buffer)
+ } else {
+ false
+ }
+ }
+
+ fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+ let suggestion = self.copilot_state.suggestion.take()?;
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
+ });
+ let buffer = self.buffer.read(cx).read(cx);
+
+ if suggestion.position.is_valid(&buffer) {
+ Some(suggestion)
+ } else {
+ None
+ }
}
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
@@ -3299,14 +3451,27 @@ impl Editor {
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
+ let text = Rope::from(text);
+ let mut to_remove = Vec::new();
+ if let Some(suggestion) = self.copilot_state.suggestion.take() {
+ to_remove.push(suggestion.id);
+ }
+
+ let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
+ let to_insert = vec![(
+ suggestion_inlay_id,
+ InlayProperties {
+ position: cursor,
+ text: text.clone(),
+ },
+ )];
self.display_map.update(cx, move |map, cx| {
- map.replace_suggestion(
- Some(Suggestion {
- position: cursor,
- text: text.trim_end().into(),
- }),
- cx,
- )
+ map.splice_inlays(to_remove, to_insert, cx)
+ });
+ self.copilot_state.suggestion = Some(Inlay {
+ id: suggestion_inlay_id,
+ position: cursor,
+ text,
});
cx.notify();
} else {
@@ -6641,7 +6806,7 @@ impl Editor {
if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
*end_selections = Some(self.selections.disjoint_anchors());
} else {
- log::error!("unexpectedly ended a transaction that wasn't started by this editor");
+ error!("unexpectedly ended a transaction that wasn't started by this editor");
}
cx.emit(Event::Edited);
@@ -7103,6 +7268,7 @@ impl Editor {
self.update_visible_copilot_suggestion(cx);
}
cx.emit(Event::BufferEdited);
+ self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
}
multi_buffer::Event::ExcerptsAdded {
buffer,
@@ -7127,7 +7293,7 @@ impl Editor {
self.refresh_active_diagnostics(cx);
}
_ => {}
- }
+ };
}
fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
@@ -7136,6 +7302,14 @@ impl Editor {
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inlays(
+ InlayRefreshReason::SettingsChange(inlay_hint_settings(
+ self.selections.newest_anchor().head(),
+ &self.buffer.read(cx).snapshot(cx),
+ cx,
+ )),
+ cx,
+ );
}
pub fn set_searchable(&mut self, searchable: bool) {
@@ -7425,6 +7599,23 @@ impl Editor {
let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
cx.write_to_clipboard(ClipboardItem::new(lines));
}
+
+ pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+ &self.inlay_hint_cache
+ }
+}
+
+fn inlay_hint_settings(
+ location: Anchor,
+ snapshot: &MultiBufferSnapshot,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+ let file = snapshot.file_at(location);
+ let language = snapshot.language_at(location);
+ let settings = all_language_settings(file, cx);
+ settings
+ .language(language.map(|l| l.name()).as_deref())
+ .inlay_hints
}
fn consume_contiguous_rows(
@@ -1392,7 +1392,12 @@ impl EditorElement {
} else {
let style = &self.style;
let chunks = snapshot
- .chunks(rows.clone(), true, Some(style.theme.suggestion))
+ .chunks(
+ rows.clone(),
+ true,
+ Some(style.theme.hint),
+ Some(style.theme.suggestion),
+ )
.map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
@@ -1921,7 +1926,7 @@ impl Element<Editor> for EditorElement {
let em_advance = style.text.em_advance(cx.font_cache());
let overscroll = vec2f(em_width, 0.);
let snapshot = {
- editor.set_visible_line_count(size.y() / line_height);
+ editor.set_visible_line_count(size.y() / line_height, cx);
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
let wrap_width = match editor.soft_wrap_mode(cx) {
@@ -0,0 +1,2021 @@
+use std::{
+ cmp,
+ ops::{ControlFlow, Range},
+ sync::Arc,
+};
+
+use crate::{
+ display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use gpui::{ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use log::error;
+use parking_lot::RwLock;
+use project::InlayHint;
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use util::post_inc;
+
+pub struct InlayHintCache {
+ pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+ pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+ pub version: usize,
+ pub enabled: bool,
+ update_tasks: HashMap<ExcerptId, UpdateTask>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+ version: usize,
+ buffer_version: Global,
+ buffer_id: u64,
+ pub hints: Vec<(InlayId, InlayHint)>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+ RefreshRequested,
+ ExcerptEdited,
+ None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+ pub to_remove: Vec<InlayId>,
+ pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
+}
+
+struct UpdateTask {
+ invalidate: InvalidationStrategy,
+ cache_version: usize,
+ task: RunningTask,
+ pending_refresh: Option<ExcerptQuery>,
+}
+
+struct RunningTask {
+ _task: Task<()>,
+ is_running_rx: smol::channel::Receiver<()>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+ excerpt_id: ExcerptId,
+ remove_from_visible: Vec<InlayId>,
+ remove_from_cache: HashSet<InlayId>,
+ add_to_cache: HashSet<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+ buffer_id: u64,
+ excerpt_id: ExcerptId,
+ dimensions: ExcerptDimensions,
+ cache_version: usize,
+ invalidate: InvalidationStrategy,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptDimensions {
+ excerpt_range_start: language::Anchor,
+ excerpt_range_end: language::Anchor,
+ excerpt_visible_range_start: language::Anchor,
+ excerpt_visible_range_end: language::Anchor,
+}
+
+struct HintFetchRanges {
+ visible_range: Range<language::Anchor>,
+ other_ranges: Vec<Range<language::Anchor>>,
+}
+
+impl InvalidationStrategy {
+ fn should_invalidate(&self) -> bool {
+ matches!(
+ self,
+ InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
+ )
+ }
+}
+
+impl ExcerptQuery {
+ fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
+ let visible_range =
+ self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
+ let mut other_ranges = Vec::new();
+ if self
+ .dimensions
+ .excerpt_range_start
+ .cmp(&visible_range.start, buffer)
+ .is_lt()
+ {
+ let mut end = visible_range.start;
+ end.offset -= 1;
+ other_ranges.push(self.dimensions.excerpt_range_start..end);
+ }
+ if self
+ .dimensions
+ .excerpt_range_end
+ .cmp(&visible_range.end, buffer)
+ .is_gt()
+ {
+ let mut start = visible_range.end;
+ start.offset += 1;
+ other_ranges.push(start..self.dimensions.excerpt_range_end);
+ }
+
+ HintFetchRanges {
+ visible_range,
+ other_ranges: other_ranges.into_iter().map(|range| range).collect(),
+ }
+ }
+}
+
+impl InlayHintCache {
+ pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+ Self {
+ allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+ enabled: inlay_hint_settings.enabled,
+ hints: HashMap::default(),
+ update_tasks: HashMap::default(),
+ version: 0,
+ }
+ }
+
+ pub fn update_settings(
+ &mut self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ new_hint_settings: InlayHintSettings,
+ visible_hints: Vec<Inlay>,
+ cx: &mut ViewContext<Editor>,
+ ) -> ControlFlow<Option<InlaySplice>> {
+ let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+ match (self.enabled, new_hint_settings.enabled) {
+ (false, false) => {
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Break(None)
+ }
+ (true, true) => {
+ if new_allowed_hint_kinds == self.allowed_hint_kinds {
+ ControlFlow::Break(None)
+ } else {
+ let new_splice = self.new_allowed_hint_kinds_splice(
+ multi_buffer,
+ &visible_hints,
+ &new_allowed_hint_kinds,
+ cx,
+ );
+ if new_splice.is_some() {
+ self.version += 1;
+ self.update_tasks.clear();
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ }
+ ControlFlow::Break(new_splice)
+ }
+ }
+ (true, false) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ if self.hints.is_empty() {
+ ControlFlow::Break(None)
+ } else {
+ self.clear();
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+ to_insert: Vec::new(),
+ }))
+ }
+ }
+ (false, true) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Continue(())
+ }
+ }
+ }
+
+ pub fn refresh_inlay_hints(
+ &mut self,
+ mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if !self.enabled || excerpts_to_query.is_empty() {
+ return;
+ }
+ let update_tasks = &mut self.update_tasks;
+ if invalidate.should_invalidate() {
+ update_tasks
+ .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+ }
+ let cache_version = self.version;
+ excerpts_to_query.retain(|visible_excerpt_id, _| {
+ match update_tasks.entry(*visible_excerpt_id) {
+ hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
+ cmp::Ordering::Less => true,
+ cmp::Ordering::Equal => invalidate.should_invalidate(),
+ cmp::Ordering::Greater => false,
+ },
+ hash_map::Entry::Vacant(_) => true,
+ }
+ });
+
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ fn new_allowed_hint_kinds_splice(
+ &self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ visible_hints: &[Inlay],
+ new_kinds: &HashSet<Option<InlayHintKind>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<InlaySplice> {
+ let old_kinds = &self.allowed_hint_kinds;
+ if new_kinds == old_kinds {
+ return None;
+ }
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let mut shown_hints_to_remove = visible_hints.iter().fold(
+ HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
+ |mut current_hints, inlay| {
+ current_hints
+ .entry(inlay.position.excerpt_id)
+ .or_default()
+ .push((inlay.position, inlay.id));
+ current_hints
+ },
+ );
+
+ let multi_buffer = multi_buffer.read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+
+ for (excerpt_id, excerpt_cached_hints) in &self.hints {
+ let shown_excerpt_hints_to_remove =
+ shown_hints_to_remove.entry(*excerpt_id).or_default();
+ let excerpt_cached_hints = excerpt_cached_hints.read();
+ let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
+ shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+ let Some(buffer) = shown_anchor
+ .buffer_id
+ .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ loop {
+ match excerpt_cache.peek() {
+ Some((cached_hint_id, cached_hint)) => {
+ if cached_hint_id == shown_hint_id {
+ excerpt_cache.next();
+ return !new_kinds.contains(&cached_hint.kind);
+ }
+
+ match cached_hint
+ .position
+ .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
+ {
+ cmp::Ordering::Less | cmp::Ordering::Equal => {
+ if !old_kinds.contains(&cached_hint.kind)
+ && new_kinds.contains(&cached_hint.kind)
+ {
+ to_insert.push((
+ multi_buffer_snapshot.anchor_in_excerpt(
+ *excerpt_id,
+ cached_hint.position,
+ ),
+ *cached_hint_id,
+ cached_hint.clone(),
+ ));
+ }
+ excerpt_cache.next();
+ }
+ cmp::Ordering::Greater => return true,
+ }
+ }
+ None => return true,
+ }
+ }
+ });
+
+ for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+ let cached_hint_kind = maybe_missed_cached_hint.kind;
+ if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
+ to_insert.push((
+ multi_buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+ *cached_hint_id,
+ maybe_missed_cached_hint.clone(),
+ ));
+ }
+ }
+ }
+
+ to_remove.extend(
+ shown_hints_to_remove
+ .into_values()
+ .flatten()
+ .map(|(_, hint_id)| hint_id),
+ );
+ if to_remove.is_empty() && to_insert.is_empty() {
+ None
+ } else {
+ Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })
+ }
+ }
+
+ fn clear(&mut self) {
+ self.version += 1;
+ self.update_tasks.clear();
+ self.hints.clear();
+ }
+}
+
+fn spawn_new_update_tasks(
+ editor: &mut Editor,
+ excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ update_cache_version: usize,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) {
+ let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+ for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
+ if !excerpt_visible_range.is_empty() {
+ let buffer = buffer_handle.read(cx);
+ let buffer_snapshot = buffer.snapshot();
+ let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let new_task_buffer_version = buffer_snapshot.version();
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+ if cached_excerpt_hints.version > update_cache_version
+ || cached_buffer_version.changed_since(new_task_buffer_version)
+ {
+ return;
+ }
+ if !new_task_buffer_version.changed_since(&cached_buffer_version)
+ && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+ {
+ return;
+ }
+ };
+
+ let buffer_id = buffer.remote_id();
+ let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+ let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+ let (multi_buffer_snapshot, full_excerpt_range) =
+ editor.buffer.update(cx, |multi_buffer, cx| {
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ (
+ multi_buffer_snapshot,
+ multi_buffer
+ .excerpts_for_buffer(&buffer_handle, cx)
+ .into_iter()
+ .find(|(id, _)| id == &excerpt_id)
+ .map(|(_, range)| range.context),
+ )
+ });
+
+ if let Some(full_excerpt_range) = full_excerpt_range {
+ let query = ExcerptQuery {
+ buffer_id,
+ excerpt_id,
+ dimensions: ExcerptDimensions {
+ excerpt_range_start: full_excerpt_range.start,
+ excerpt_range_end: full_excerpt_range.end,
+ excerpt_visible_range_start,
+ excerpt_visible_range_end,
+ },
+ cache_version: update_cache_version,
+ invalidate,
+ };
+
+ let new_update_task = |is_refresh_after_regular_task| {
+ new_update_task(
+ query,
+ multi_buffer_snapshot,
+ buffer_snapshot,
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints,
+ is_refresh_after_regular_task,
+ cx,
+ )
+ };
+ match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ let update_task = o.get_mut();
+ match (update_task.invalidate, invalidate) {
+ (_, InvalidationStrategy::None) => {}
+ (
+ InvalidationStrategy::ExcerptEdited,
+ InvalidationStrategy::RefreshRequested,
+ ) if !update_task.task.is_running_rx.is_closed() => {
+ update_task.pending_refresh = Some(query);
+ }
+ _ => {
+ o.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
+ }
+ }
+ }
+ hash_map::Entry::Vacant(v) => {
+ v.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
+ }
+ }
+ }
+ }
+ }
+}
+
+fn new_update_task(
+ query: ExcerptQuery,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ is_refresh_after_regular_task: bool,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> RunningTask {
+ let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
+ let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
+ let _task = cx.spawn(|editor, mut cx| async move {
+ let _is_running_tx = is_running_tx;
+ let create_update_task = |range| {
+ fetch_and_update_hints(
+ editor.clone(),
+ multi_buffer_snapshot.clone(),
+ buffer_snapshot.clone(),
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints.as_ref().map(Arc::clone),
+ query,
+ range,
+ cx.clone(),
+ )
+ };
+
+ if is_refresh_after_regular_task {
+ let visible_range_has_updates =
+ match create_update_task(hints_fetch_ranges.visible_range).await {
+ Ok(updated) => updated,
+ Err(e) => {
+ error!("inlay hint visible range update task failed: {e:#}");
+ return;
+ }
+ };
+
+ if visible_range_has_updates {
+ let other_update_results = futures::future::join_all(
+ hints_fetch_ranges
+ .other_ranges
+ .into_iter()
+ .map(create_update_task),
+ )
+ .await;
+
+ for result in other_update_results {
+ if let Err(e) = result {
+ error!("inlay hint update task failed: {e:#}");
+ }
+ }
+ }
+ } else {
+ let task_update_results = futures::future::join_all(
+ std::iter::once(hints_fetch_ranges.visible_range)
+ .chain(hints_fetch_ranges.other_ranges.into_iter())
+ .map(create_update_task),
+ )
+ .await;
+
+ for result in task_update_results {
+ if let Err(e) = result {
+ error!("inlay hint update task failed: {e:#}");
+ }
+ }
+ }
+
+ editor
+ .update(&mut cx, |editor, cx| {
+ let pending_refresh_query = editor
+ .inlay_hint_cache
+ .update_tasks
+ .get_mut(&query.excerpt_id)
+ .and_then(|task| task.pending_refresh.take());
+
+ if let Some(pending_refresh_query) = pending_refresh_query {
+ let refresh_multi_buffer = editor.buffer().read(cx);
+ let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
+ let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+ let refresh_cached_excerpt_hints = editor
+ .inlay_hint_cache
+ .hints
+ .get(&pending_refresh_query.excerpt_id)
+ .map(Arc::clone);
+ if let Some(buffer) =
+ refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
+ {
+ drop(refresh_multi_buffer);
+ editor.inlay_hint_cache.update_tasks.insert(
+ pending_refresh_query.excerpt_id,
+ UpdateTask {
+ invalidate: InvalidationStrategy::RefreshRequested,
+ cache_version: editor.inlay_hint_cache.version,
+ task: new_update_task(
+ pending_refresh_query,
+ refresh_multi_buffer_snapshot,
+ buffer.read(cx).snapshot(),
+ refresh_visible_hints,
+ refresh_cached_excerpt_hints,
+ true,
+ cx,
+ ),
+ pending_refresh: None,
+ },
+ );
+ }
+ }
+ })
+ .ok();
+ });
+
+ RunningTask {
+ _task,
+ is_running_rx,
+ }
+}
+
+async fn fetch_and_update_hints(
+ editor: gpui::WeakViewHandle<Editor>,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ query: ExcerptQuery,
+ fetch_range: Range<language::Anchor>,
+ mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<bool> {
+ let inlay_hints_fetch_task = editor
+ .update(&mut cx, |editor, cx| {
+ 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)
+ }))
+ })
+ })
+ .ok()
+ .flatten();
+ let mut update_happened = false;
+ let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
+ let new_hints = inlay_hints_fetch_task
+ .await
+ .context("inlay hint fetch task")?;
+ let background_task_buffer_snapshot = buffer_snapshot.clone();
+ let backround_fetch_range = fetch_range.clone();
+ let new_update = cx
+ .background()
+ .spawn(async move {
+ calculate_hint_updates(
+ query,
+ backround_fetch_range,
+ new_hints,
+ &background_task_buffer_snapshot,
+ cached_excerpt_hints,
+ &visible_hints,
+ )
+ })
+ .await;
+
+ editor
+ .update(&mut cx, |editor, cx| {
+ if let Some(new_update) = new_update {
+ update_happened = !new_update.add_to_cache.is_empty()
+ || !new_update.remove_from_cache.is_empty()
+ || !new_update.remove_from_visible.is_empty();
+
+ let cached_excerpt_hints = editor
+ .inlay_hint_cache
+ .hints
+ .entry(new_update.excerpt_id)
+ .or_insert_with(|| {
+ Arc::new(RwLock::new(CachedExcerptHints {
+ version: query.cache_version,
+ buffer_version: buffer_snapshot.version().clone(),
+ buffer_id: query.buffer_id,
+ hints: Vec::new(),
+ }))
+ });
+ let mut cached_excerpt_hints = cached_excerpt_hints.write();
+ match query.cache_version.cmp(&cached_excerpt_hints.version) {
+ cmp::Ordering::Less => return,
+ cmp::Ordering::Greater | cmp::Ordering::Equal => {
+ cached_excerpt_hints.version = query.cache_version;
+ }
+ }
+ cached_excerpt_hints
+ .hints
+ .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+ cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+ editor.inlay_hint_cache.version += 1;
+
+ let mut splice = InlaySplice {
+ to_remove: new_update.remove_from_visible,
+ to_insert: Vec::new(),
+ };
+
+ for new_hint in new_update.add_to_cache {
+ let new_hint_position = multi_buffer_snapshot
+ .anchor_in_excerpt(query.excerpt_id, new_hint.position);
+ let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
+ if editor
+ .inlay_hint_cache
+ .allowed_hint_kinds
+ .contains(&new_hint.kind)
+ {
+ splice
+ .to_insert
+ .push((new_hint_position, new_inlay_id, new_hint.clone()));
+ }
+
+ cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
+ }
+
+ cached_excerpt_hints
+ .hints
+ .sort_by(|(_, hint_a), (_, hint_b)| {
+ hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
+ });
+ drop(cached_excerpt_hints);
+
+ if query.invalidate.should_invalidate() {
+ let mut outdated_excerpt_caches = HashSet::default();
+ for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
+ let excerpt_hints = excerpt_hints.read();
+ if excerpt_hints.buffer_id == query.buffer_id
+ && excerpt_id != &query.excerpt_id
+ && buffer_snapshot
+ .version()
+ .changed_since(&excerpt_hints.buffer_version)
+ {
+ outdated_excerpt_caches.insert(*excerpt_id);
+ splice
+ .to_remove
+ .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+ }
+ }
+ editor
+ .inlay_hint_cache
+ .hints
+ .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+ }
+
+ let InlaySplice {
+ to_remove,
+ to_insert,
+ } = splice;
+ if !to_remove.is_empty() || !to_insert.is_empty() {
+ editor.splice_inlay_hints(to_remove, to_insert, cx)
+ }
+ }
+ })
+ .ok();
+
+ Ok(update_happened)
+}
+
+fn calculate_hint_updates(
+ query: ExcerptQuery,
+ fetch_range: Range<language::Anchor>,
+ new_excerpt_hints: Vec<InlayHint>,
+ buffer_snapshot: &BufferSnapshot,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ visible_hints: &[Inlay],
+) -> Option<ExcerptHintsUpdate> {
+ let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
+ let mut excerpt_hints_to_persist = HashMap::default();
+ for new_hint in new_excerpt_hints {
+ if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
+ continue;
+ }
+ let missing_from_cache = match &cached_excerpt_hints {
+ Some(cached_excerpt_hints) => {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ match cached_excerpt_hints.hints.binary_search_by(|probe| {
+ probe.1.position.cmp(&new_hint.position, buffer_snapshot)
+ }) {
+ Ok(ix) => {
+ let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
+ if cached_hint == &new_hint {
+ excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+ false
+ } else {
+ true
+ }
+ }
+ Err(_) => true,
+ }
+ }
+ None => true,
+ };
+ if missing_from_cache {
+ add_to_cache.insert(new_hint);
+ }
+ }
+
+ let mut remove_from_visible = Vec::new();
+ let mut remove_from_cache = HashSet::default();
+ if query.invalidate.should_invalidate() {
+ remove_from_visible.extend(
+ visible_hints
+ .iter()
+ .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
+ .filter(|hint| {
+ contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
+ })
+ .filter(|hint| {
+ fetch_range
+ .start
+ .cmp(&hint.position.text_anchor, buffer_snapshot)
+ .is_le()
+ && fetch_range
+ .end
+ .cmp(&hint.position.text_anchor, buffer_snapshot)
+ .is_ge()
+ })
+ .map(|inlay_hint| inlay_hint.id)
+ .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+ );
+
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ remove_from_cache.extend(
+ cached_excerpt_hints
+ .hints
+ .iter()
+ .filter(|(cached_inlay_id, _)| {
+ !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+ })
+ .filter(|(_, cached_hint)| {
+ fetch_range
+ .start
+ .cmp(&cached_hint.position, buffer_snapshot)
+ .is_le()
+ && fetch_range
+ .end
+ .cmp(&cached_hint.position, buffer_snapshot)
+ .is_ge()
+ })
+ .map(|(cached_inlay_id, _)| *cached_inlay_id),
+ );
+ }
+ }
+
+ if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+ None
+ } else {
+ Some(ExcerptHintsUpdate {
+ excerpt_id: query.excerpt_id,
+ remove_from_visible,
+ remove_from_cache,
+ add_to_cache,
+ })
+ }
+}
+
+fn contains_position(
+ range: &Range<language::Anchor>,
+ position: language::Anchor,
+ buffer_snapshot: &BufferSnapshot,
+) -> bool {
+ range.start.cmp(&position, buffer_snapshot).is_le()
+ && range.end.cmp(&position, buffer_snapshot).is_ge()
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+
+ use crate::{
+ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+ serde_json::json,
+ ExcerptRange, InlayHintSettings,
+ };
+ use futures::StreamExt;
+ use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+ use language::{
+ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+ };
+ use lsp::FakeLanguageServer;
+ use parking_lot::Mutex;
+ use project::{FakeFs, Project};
+ use settings::SettingsStore;
+ use text::Point;
+ use workspace::Workspace;
+
+ use crate::editor_tests::update_test_settings;
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ let current_call_id =
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+ for _ in 0..2 {
+ let mut i = current_call_id;
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if i == 0 {
+ break;
+ }
+ i -= 1;
+ }
+ }
+
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some change", cx);
+ edits_made += 1;
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after an edit"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should get new hints after hint refresh/ request"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 1),
+ label: lsp::InlayHintLabel::String("type hint".to_string()),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 2),
+ label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+ kind: Some(lsp::InlayHintKind::PARAMETER),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 3),
+ label: lsp::InlayHintLabel::String("other hint".to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ ]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 1,
+ "Should query new hints once"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should load new hints twice"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Cached hints should not change due to allowed hint kinds settings update"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Should not update cache version due to new loaded hints being the same"
+ );
+ });
+
+ for (new_allowed_hint_kinds, expected_visible_hints) in [
+ (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type)]),
+ vec!["type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+ vec!["other hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+ vec!["other hint".to_string(), "parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([
+ None,
+ Some(InlayHintKind::Type),
+ Some(InlayHintKind::Parameter),
+ ]),
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ ),
+ ] {
+ edits_made += 1;
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: new_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: new_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ expected_visible_hints,
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+ );
+ });
+ }
+
+ edits_made += 1;
+ let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: another_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: another_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when hints got disabled"
+ );
+ assert!(
+ cached_hint_labels(editor).is_empty(),
+ "Should clear the cache when hints got disabled"
+ );
+ assert!(
+ visible_hint_labels(editor, cx).is_empty(),
+ "Should clear visible hints when hints got disabled"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+ "Should update its allowed hint kinds even when hints got disabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after hints got disabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when they got disabled"
+ );
+ assert!(cached_hint_labels(editor).is_empty());
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should not update the cache version after /refresh query without updates"
+ );
+ });
+
+ let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+ edits_made += 1;
+ update_test_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: final_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: final_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 3,
+ "Should query for new hints when they got reenabled"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints fully repopulated after the hints got reenabled"
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints repopulated and filtered after the h"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+ "Cache should update editor settings when hints got reenabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Cache should update its version after hints got reenabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 4,
+ "Should query for new hints again"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, edits_made);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let fake_server = Arc::new(fake_server);
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+
+ let mut expected_changes = Vec::new();
+ for change_after_opening in [
+ "initial change #1",
+ "initial change #2",
+ "initial change #3",
+ ] {
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(change_after_opening, cx);
+ });
+ expected_changes.push(change_after_opening);
+ }
+
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+ );
+ let expected_hints = vec!["2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 1,
+ "Only one update should be registered in the cache after all cancellations"
+ );
+ });
+
+ let mut edits = Vec::new();
+ for async_later_change in [
+ "another change #1",
+ "another change #2",
+ "another change #3",
+ ] {
+ expected_changes.push(async_later_change);
+ let task_editor = editor.clone();
+ let mut task_cx = cx.clone();
+ edits.push(cx.foreground().spawn(async move {
+ task_editor.update(&mut task_cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(async_later_change, cx);
+ });
+ }));
+ }
+ let _ = futures::future::join_all(edits).await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::SeqCst),
+ 3,
+ "Should query new hints one more time, for the last edit only"
+ );
+ let expected_hints = vec!["3".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 2,
+ "Should update the cache version once more, for the new change"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+ let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+ let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+
+ task_lsp_request_ranges.lock().push(params.range);
+ let query_start = params.range.start;
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ Ok(Some(vec![lsp::InlayHint {
+ position: query_start,
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ ranges.sort_by_key(|range| range.start);
+ assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+ assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+ assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
+ assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
+
+ assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
+ "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+ let expected_layers = vec!["1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "Should have hints from both LSP requests made for a big file"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(
+ inlay_cache.version, 2,
+ "Both LSP queries should've bumped the cache version"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
+ editor.handle_input("++++more text++++", cx);
+ });
+
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ ranges.sort_by_key(|range| range.start);
+ assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
+ assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+ assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
+ assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
+ assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
+ assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
+ assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
+
+ assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
+ "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
+ let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "Should have hints from the new LSP response after edit");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_multiple_excerpts_large_multibuffer(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::clone(&language))
+ });
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "main.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "other.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 0)..Point::new(11, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 0)..Point::new(33, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 0)..Point::new(55, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 0)..Point::new(66, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 0)..Point::new(77, 0),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 1)..Point::new(2, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 1)..Point::new(11, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 1)..Point::new(33, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 1)..Point::new(55, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 1)..Point::new(66, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 1)..Point::new(77, 1),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+
+ deterministic.run_until_parked();
+ cx.foreground().run_until_parked();
+ let (_, editor) =
+ cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+ let editor_edited = Arc::new(AtomicBool::new(false));
+ let fake_server = fake_servers.next().await.unwrap();
+ let closure_editor_edited = Arc::clone(&editor_edited);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_editor_edited = Arc::clone(&closure_editor_edited);
+ async move {
+ let hint_text = if params.text_document.uri
+ == lsp::Url::from_file_path("/a/main.rs").unwrap()
+ {
+ "main hint"
+ } else if params.text_document.uri
+ == lsp::Url::from_file_path("/a/other.rs").unwrap()
+ {
+ "other hint"
+ } else {
+ panic!("unexpected uri: {:?}", params.text_document.uri);
+ };
+
+ let positions = [
+ lsp::Position::new(0, 2),
+ lsp::Position::new(4, 2),
+ lsp::Position::new(22, 2),
+ lsp::Position::new(44, 2),
+ lsp::Position::new(56, 2),
+ lsp::Position::new(67, 2),
+ ];
+ let out_of_range_hint = lsp::InlayHint {
+ position: lsp::Position::new(
+ params.range.start.line + 99,
+ params.range.start.character + 99,
+ ),
+ label: lsp::InlayHintLabel::String(
+ "out of excerpt range, should be ignored".to_string(),
+ ),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ };
+
+ let edited = task_editor_edited.load(Ordering::Acquire);
+ Ok(Some(
+ std::iter::once(out_of_range_hint)
+ .chain(positions.into_iter().enumerate().map(|(i, position)| {
+ lsp::InlayHint {
+ position,
+ label: lsp::InlayHintLabel::String(format!(
+ "{hint_text}{} #{i}",
+ if edited { "(edited)" } else { "" },
+ )),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }
+ }))
+ .collect(),
+ ))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ ];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+ );
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 9);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 12);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
+ });
+
+ editor_edited.store(true, Ordering::Release);
+ editor.update(cx, |editor, cx| {
+ editor.handle_input("++++more text++++", cx);
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec![
+ "main hint(edited) #0".to_string(),
+ "main hint(edited) #1".to_string(),
+ "main hint(edited) #2".to_string(),
+ "main hint(edited) #3".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_layers, cached_hint_labels(editor),
+ "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
+unedited (2nd) buffer should have the same hint");
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+ assert_eq!(inlay_cache.version, 16);
+ });
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ crate::init(cx);
+ });
+
+ update_test_settings(cx, f);
+ }
+
+ async fn prepare_test_objects(
+ cx: &mut TestAppContext,
+ ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ ("/a/main.rs", editor, fake_server)
+ }
+
+ fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for (_, inlay) in excerpt_hints.hints.iter() {
+ match &inlay.label {
+ project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+ _ => unreachable!(),
+ }
+ }
+ }
+
+ labels.sort();
+ labels
+ }
+
+ fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+ let mut hints = editor
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .map(|hint| hint.text.to_string())
+ .collect::<Vec<_>>();
+ hints.sort();
+ hints
+ }
+}
@@ -49,6 +49,10 @@ impl Anchor {
}
}
+ pub fn bias(&self) -> Bias {
+ self.text_anchor.bias
+ }
+
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Left {
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
@@ -81,6 +85,19 @@ impl Anchor {
{
snapshot.summary_for_anchor(self)
}
+
+ pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
+ if *self == Anchor::min() || *self == Anchor::max() {
+ true
+ } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
+ excerpt.contains(self)
+ && (self.text_anchor == excerpt.range.context.start
+ || self.text_anchor == excerpt.range.context.end
+ || self.text_anchor.is_valid(&excerpt.buffer))
+ } else {
+ false
+ }
+ }
}
impl ToOffset for Anchor {
@@ -13,13 +13,14 @@ use gpui::{
};
use language::{Bias, Point};
use util::ResultExt;
-use workspace::WorkspaceId;
+use workspace::{item::Item, WorkspaceId};
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
- Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
+ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+ ToPoint,
};
use self::{
@@ -293,8 +294,19 @@ impl Editor {
self.scroll_manager.visible_line_count
}
- pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
- self.scroll_manager.visible_line_count = Some(lines)
+ pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+ let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+ self.scroll_manager.visible_line_count = Some(lines);
+ if opened_first_time {
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+ })
+ .ok()
+ })
+ .detach()
+ }
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
@@ -320,6 +332,10 @@ impl Editor {
workspace_id,
cx,
);
+
+ if !self.is_singleton(cx) {
+ self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+ }
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -1,6 +1,6 @@
use crate::{File, Language};
use anyhow::Result;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
use globset::GlobMatcher;
use gpui::AppContext;
use schemars::{
@@ -52,6 +52,7 @@ pub struct LanguageSettings {
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
pub extend_comment_on_newline: bool,
+ pub inlay_hints: InlayHintSettings,
}
#[derive(Clone, Debug, Default)]
@@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
pub show_whitespaces: Option<ShowWhitespaceSetting>,
#[serde(default)]
pub extend_comment_on_newline: Option<bool>,
+ #[serde(default)]
+ pub inlay_hints: Option<InlayHintSettings>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -150,6 +153,38 @@ pub enum Formatter {
},
}
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettings {
+ #[serde(default)]
+ pub enabled: bool,
+ #[serde(default = "default_true")]
+ pub show_type_hints: bool,
+ #[serde(default = "default_true")]
+ pub show_parameter_hints: bool,
+ #[serde(default = "default_true")]
+ pub show_other_hints: bool,
+}
+
+fn default_true() -> bool {
+ true
+}
+
+impl InlayHintSettings {
+ pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
+ let mut kinds = HashSet::default();
+ if self.show_type_hints {
+ kinds.insert(Some(InlayHintKind::Type));
+ }
+ if self.show_parameter_hints {
+ kinds.insert(Some(InlayHintKind::Parameter));
+ }
+ if self.show_other_hints {
+ kinds.insert(None);
+ }
+ kinds
+ }
+}
+
impl AllLanguageSettings {
pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
if let Some(name) = language_name {
@@ -184,6 +219,29 @@ impl AllLanguageSettings {
}
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+ Type,
+ Parameter,
+}
+
+impl InlayHintKind {
+ pub fn from_name(name: &str) -> Option<Self> {
+ match name {
+ "type" => Some(InlayHintKind::Type),
+ "parameter" => Some(InlayHintKind::Parameter),
+ _ => None,
+ }
+ }
+
+ pub fn name(&self) -> &'static str {
+ match self {
+ InlayHintKind::Type => "type",
+ InlayHintKind::Parameter => "parameter",
+ }
+ }
+}
+
impl settings::Setting for AllLanguageSettings {
const KEY: Option<&'static str> = None;
@@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
);
+ merge(&mut settings.inlay_hints, src.inlay_hints);
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
@@ -388,6 +388,9 @@ impl LanguageServer {
resolve_support: None,
..WorkspaceSymbolClientCapabilities::default()
}),
+ inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
+ refresh_support: Some(true),
+ }),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {
@@ -429,6 +432,10 @@ impl LanguageServer {
content_format: Some(vec![MarkupKind::Markdown]),
..Default::default()
}),
+ inlay_hint: Some(InlayHintClientCapabilities {
+ resolve_support: None,
+ dynamic_registration: Some(false),
+ }),
..Default::default()
}),
experimental: Some(json!({
@@ -607,6 +614,7 @@ impl LanguageServer {
})
.detach();
}
+
Err(error) => {
log::error!(
"error deserializing {} request: {:?}, message: {:?}",
@@ -708,7 +716,7 @@ impl LanguageServer {
.context("failed to deserialize response"),
Err(error) => Err(anyhow!("{}", error.message)),
};
- let _ = tx.send(response);
+ _ = tx.send(response);
})
.detach();
}),
@@ -1,14 +1,15 @@
use crate::{
- DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
- ProjectTransaction,
+ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+ InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+ MarkupContent, Project, ProjectTransaction,
};
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use fs::LineEnding;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
- language_settings::language_settings,
+ language_settings::{language_settings, InlayHintKind},
point_from_lsp, point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
@@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting {
pub push_to_history: bool,
}
+pub(crate) struct InlayHints {
+ pub range: Range<Anchor>,
+}
+
pub(crate) struct FormattingOptions {
tab_size: u32,
}
@@ -1780,3 +1785,327 @@ impl LspCommand for OnTypeFormatting {
message.buffer_id
}
}
+
+#[async_trait(?Send)]
+impl LspCommand for InlayHints {
+ type Response = Vec<InlayHint>;
+ type LspRequest = lsp::InlayHintRequest;
+ type ProtoRequest = proto::InlayHints;
+
+ fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
+ let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
+ match inlay_hint_provider {
+ lsp::OneOf::Left(enabled) => *enabled,
+ lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
+ lsp::InlayHintServerCapabilities::Options(_) => true,
+ lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false,
+ },
+ }
+ }
+
+ fn to_lsp(
+ &self,
+ path: &Path,
+ buffer: &Buffer,
+ _: &Arc<LanguageServer>,
+ _: &AppContext,
+ ) -> lsp::InlayHintParams {
+ lsp::InlayHintParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: lsp::Url::from_file_path(path).unwrap(),
+ },
+ range: range_to_lsp(self.range.to_point_utf16(buffer)),
+ work_done_progress_params: Default::default(),
+ }
+ }
+
+ async fn response_from_lsp(
+ self,
+ message: Option<Vec<lsp::InlayHint>>,
+ _: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ _: LanguageServerId,
+ cx: AsyncAppContext,
+ ) -> Result<Vec<InlayHint>> {
+ cx.read(|cx| {
+ let origin_buffer = buffer.read(cx);
+ Ok(message
+ .unwrap_or_default()
+ .into_iter()
+ .map(|lsp_hint| {
+ let kind = lsp_hint.kind.and_then(|kind| match kind {
+ lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+ lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+ _ => None,
+ });
+ let position = origin_buffer
+ .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+ InlayHint {
+ buffer_id: origin_buffer.remote_id(),
+ position: if kind == Some(InlayHintKind::Parameter) {
+ origin_buffer.anchor_before(position)
+ } else {
+ origin_buffer.anchor_after(position)
+ },
+ padding_left: lsp_hint.padding_left.unwrap_or(false),
+ padding_right: lsp_hint.padding_right.unwrap_or(false),
+ label: match lsp_hint.label {
+ lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+ lsp::InlayHintLabel::LabelParts(lsp_parts) => {
+ InlayHintLabel::LabelParts(
+ lsp_parts
+ .into_iter()
+ .map(|label_part| InlayHintLabelPart {
+ value: label_part.value,
+ tooltip: label_part.tooltip.map(
+ |tooltip| {
+ match tooltip {
+ lsp::InlayHintLabelPartTooltip::String(s) => {
+ InlayHintLabelPartTooltip::String(s)
+ }
+ lsp::InlayHintLabelPartTooltip::MarkupContent(
+ markup_content,
+ ) => InlayHintLabelPartTooltip::MarkupContent(
+ MarkupContent {
+ kind: format!("{:?}", markup_content.kind),
+ value: markup_content.value,
+ },
+ ),
+ }
+ },
+ ),
+ location: label_part.location.map(|lsp_location| {
+ let target_start = origin_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.start),
+ Bias::Left,
+ );
+ let target_end = origin_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.end),
+ Bias::Left,
+ );
+ Location {
+ buffer: buffer.clone(),
+ range: origin_buffer.anchor_after(target_start)
+ ..origin_buffer.anchor_before(target_end),
+ }
+ }),
+ })
+ .collect(),
+ )
+ }
+ },
+ kind,
+ tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+ lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+ lsp::InlayHintTooltip::MarkupContent(markup_content) => {
+ InlayHintTooltip::MarkupContent(MarkupContent {
+ kind: format!("{:?}", markup_content.kind),
+ value: markup_content.value,
+ })
+ }
+ }),
+ }
+ })
+ .collect())
+ })
+ }
+
+ fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
+ proto::InlayHints {
+ project_id,
+ buffer_id: buffer.remote_id(),
+ start: Some(language::proto::serialize_anchor(&self.range.start)),
+ end: Some(language::proto::serialize_anchor(&self.range.end)),
+ version: serialize_version(&buffer.version()),
+ }
+ }
+
+ async fn from_proto(
+ message: proto::InlayHints,
+ _: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Self> {
+ let start = message
+ .start
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid start")?;
+ let end = message
+ .end
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid end")?;
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })
+ .await?;
+
+ Ok(Self { range: start..end })
+ }
+
+ fn response_to_proto(
+ response: Vec<InlayHint>,
+ _: &mut Project,
+ _: PeerId,
+ buffer_version: &clock::Global,
+ cx: &mut AppContext,
+ ) -> proto::InlayHintsResponse {
+ proto::InlayHintsResponse {
+ hints: response
+ .into_iter()
+ .map(|response_hint| proto::InlayHint {
+ position: Some(language::proto::serialize_anchor(&response_hint.position)),
+ padding_left: response_hint.padding_left,
+ padding_right: response_hint.padding_right,
+ label: Some(proto::InlayHintLabel {
+ label: Some(match response_hint.label {
+ InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+ InlayHintLabel::LabelParts(label_parts) => {
+ proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+ parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
+ value: label_part.value,
+ tooltip: label_part.tooltip.map(|tooltip| {
+ let proto_tooltip = match tooltip {
+ InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+ InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ }),
+ };
+ proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+ }),
+ location: label_part.location.map(|location| proto::Location {
+ start: Some(serialize_anchor(&location.range.start)),
+ end: Some(serialize_anchor(&location.range.end)),
+ buffer_id: location.buffer.read(cx).remote_id(),
+ }),
+ }).collect()
+ })
+ }
+ }),
+ }),
+ kind: response_hint.kind.map(|kind| kind.name().to_string()),
+ tooltip: response_hint.tooltip.map(|response_tooltip| {
+ let proto_tooltip = match response_tooltip {
+ InlayHintTooltip::String(s) => {
+ proto::inlay_hint_tooltip::Content::Value(s)
+ }
+ InlayHintTooltip::MarkupContent(markup_content) => {
+ proto::inlay_hint_tooltip::Content::MarkupContent(
+ proto::MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ },
+ )
+ }
+ };
+ proto::InlayHintTooltip {
+ content: Some(proto_tooltip),
+ }
+ }),
+ })
+ .collect(),
+ version: serialize_version(buffer_version),
+ }
+ }
+
+ async fn response_from_proto(
+ self,
+ message: proto::InlayHintsResponse,
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Vec<InlayHint>> {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })
+ .await?;
+
+ let mut hints = Vec::new();
+ for message_hint in message.hints {
+ let buffer_id = message_hint
+ .position
+ .as_ref()
+ .and_then(|location| location.buffer_id)
+ .context("missing buffer id")?;
+ let hint = InlayHint {
+ buffer_id,
+ position: message_hint
+ .position
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid position")?,
+ label: match message_hint
+ .label
+ .and_then(|label| label.label)
+ .context("missing label")?
+ {
+ proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+ proto::inlay_hint_label::Label::LabelParts(parts) => {
+ let mut label_parts = Vec::new();
+ for part in parts.parts {
+ label_parts.push(InlayHintLabelPart {
+ value: part.value,
+ tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+ Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
+ Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ }),
+ None => InlayHintLabelPartTooltip::String(String::new()),
+ }),
+ location: match part.location {
+ Some(location) => {
+ let target_buffer = project
+ .update(&mut cx, |this, cx| {
+ this.wait_for_remote_buffer(location.buffer_id, cx)
+ })
+ .await?;
+ Some(Location {
+ range: location
+ .start
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid start")?
+ ..location
+ .end
+ .and_then(language::proto::deserialize_anchor)
+ .context("invalid end")?,
+ buffer: target_buffer,
+ })},
+ None => None,
+ },
+ });
+ }
+
+ InlayHintLabel::LabelParts(label_parts)
+ }
+ },
+ padding_left: message_hint.padding_left,
+ padding_right: message_hint.padding_right,
+ kind: message_hint
+ .kind
+ .as_deref()
+ .and_then(InlayHintKind::from_name),
+ tooltip: message_hint.tooltip.and_then(|tooltip| {
+ Some(match tooltip.content? {
+ proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+ proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+ InlayHintTooltip::MarkupContent(MarkupContent {
+ kind: markup_content.kind,
+ value: markup_content.value,
+ })
+ }
+ })
+ }),
+ };
+
+ hints.push(hint);
+ }
+
+ Ok(hints)
+ }
+
+ fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
+ message.buffer_id
+ }
+}
@@ -29,14 +29,15 @@ use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
ModelHandle, Task, WeakModelHandle,
};
+use itertools::Itertools;
use language::{
- language_settings::{language_settings, FormatOnSave, Formatter},
+ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version,
},
- range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
+ range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
@@ -75,6 +76,7 @@ use std::{
time::{Duration, Instant},
};
use terminals::Terminals;
+use text::Anchor;
use util::{
debug_panic, defer, http::HttpClient, merge_json_value_into,
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
@@ -277,6 +279,7 @@ pub enum Event {
new_peer_id: proto::PeerId,
},
CollaboratorLeft(proto::PeerId),
+ RefreshInlays,
}
pub enum LanguageServerState {
@@ -319,12 +322,63 @@ pub struct DiagnosticSummary {
pub warning_count: usize,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Location {
pub buffer: ModelHandle<Buffer>,
pub range: Range<language::Anchor>,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHint {
+ pub buffer_id: u64,
+ pub position: language::Anchor,
+ pub label: InlayHintLabel,
+ pub kind: Option<InlayHintKind>,
+ pub padding_left: bool,
+ pub padding_right: bool,
+ pub tooltip: Option<InlayHintTooltip>,
+}
+
+impl InlayHint {
+ pub fn text(&self) -> String {
+ match &self.label {
+ InlayHintLabel::String(s) => s.to_owned(),
+ InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabel {
+ String(String),
+ LabelParts(Vec<InlayHintLabelPart>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHintLabelPart {
+ pub value: String,
+ pub tooltip: Option<InlayHintLabelPartTooltip>,
+ pub location: Option<Location>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintTooltip {
+ String(String),
+ MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabelPartTooltip {
+ String(String),
+ MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MarkupContent {
+ pub kind: String,
+ pub value: String,
+}
+
#[derive(Debug, Clone)]
pub struct LocationLink {
pub origin: Option<Location>,
@@ -485,6 +539,8 @@ impl Project {
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_on_type_formatting);
+ client.add_model_request_handler(Self::handle_inlay_hints);
+ client.add_model_request_handler(Self::handle_refresh_inlay_hints);
client.add_model_request_handler(Self::handle_reload_buffers);
client.add_model_request_handler(Self::handle_synchronize_buffers);
client.add_model_request_handler(Self::handle_format_buffers);
@@ -2651,10 +2707,11 @@ impl Project {
cx: &mut AsyncAppContext,
) -> Result<Option<Arc<LanguageServer>>> {
let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
-
let language_server = match pending_server.task.await? {
Some(server) => server.initialize(initialization_options).await?,
- None => return Ok(None),
+ None => {
+ return Ok(None);
+ }
};
language_server
@@ -2771,6 +2828,24 @@ impl Project {
})
.detach();
+ language_server
+ .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
+ move |(), mut cx| async move {
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ this.update(&mut cx, |project, cx| {
+ cx.emit(Event::RefreshInlays);
+ project.remote_id().map(|project_id| {
+ project.client.send(proto::RefreshInlayHints { project_id })
+ })
+ })
+ .transpose()?;
+ Ok(())
+ }
+ })
+ .detach();
+
let disk_based_diagnostics_progress_token =
adapter.disk_based_diagnostics_progress_token.clone();
@@ -4837,6 +4912,61 @@ impl Project {
)
}
+ pub fn inlay_hints<T: ToOffset>(
+ &self,
+ buffer_handle: ModelHandle<Buffer>,
+ range: Range<T>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<InlayHint>>> {
+ let buffer = buffer_handle.read(cx);
+ let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+ let range_start = range.start;
+ let range_end = range.end;
+ let buffer_id = buffer.remote_id();
+ let buffer_version = buffer.version().clone();
+ let lsp_request = InlayHints { range };
+
+ if self.is_local() {
+ let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx);
+ cx.spawn(|_, mut cx| async move {
+ buffer_handle
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
+ })
+ .await
+ .context("waiting for inlay hint request range edits")?;
+ lsp_request_task.await.context("inlay hints LSP request")
+ })
+ } else if let Some(project_id) = self.remote_id() {
+ let client = self.client.clone();
+ let request = proto::InlayHints {
+ project_id,
+ buffer_id,
+ start: Some(serialize_anchor(&range_start)),
+ end: Some(serialize_anchor(&range_end)),
+ version: serialize_version(&buffer_version),
+ };
+ cx.spawn(|project, cx| async move {
+ let response = client
+ .request(request)
+ .await
+ .context("inlay hints proto request")?;
+ let hints_request_result = LspCommand::response_from_proto(
+ lsp_request,
+ response,
+ project,
+ buffer_handle.clone(),
+ cx,
+ )
+ .await;
+
+ hints_request_result.context("inlay hints proto response conversion")
+ })
+ } else {
+ Task::ready(Err(anyhow!("project does not have a remote id")))
+ }
+ }
+
#[allow(clippy::type_complexity)]
pub fn search(
&self,
@@ -5409,41 +5539,39 @@ impl Project {
let abs_path = worktree_handle.read(cx).abs_path();
for server_id in &language_server_ids {
- if let Some(server) = self.language_servers.get(server_id) {
- if let LanguageServerState::Running {
- server,
- watched_paths,
- ..
- } = server
- {
- if let Some(watched_paths) = watched_paths.get(&worktree_id) {
- let params = lsp::DidChangeWatchedFilesParams {
- changes: changes
- .iter()
- .filter_map(|(path, _, change)| {
- if !watched_paths.is_match(&path) {
- return None;
- }
- let typ = match change {
- PathChange::Loaded => return None,
- PathChange::Added => lsp::FileChangeType::CREATED,
- PathChange::Removed => lsp::FileChangeType::DELETED,
- PathChange::Updated => lsp::FileChangeType::CHANGED,
- PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
- };
- Some(lsp::FileEvent {
- uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
- typ,
- })
+ if let Some(LanguageServerState::Running {
+ server,
+ watched_paths,
+ ..
+ }) = self.language_servers.get(server_id)
+ {
+ if let Some(watched_paths) = watched_paths.get(&worktree_id) {
+ let params = lsp::DidChangeWatchedFilesParams {
+ changes: changes
+ .iter()
+ .filter_map(|(path, _, change)| {
+ if !watched_paths.is_match(&path) {
+ return None;
+ }
+ let typ = match change {
+ PathChange::Loaded => return None,
+ PathChange::Added => lsp::FileChangeType::CREATED,
+ PathChange::Removed => lsp::FileChangeType::DELETED,
+ PathChange::Updated => lsp::FileChangeType::CHANGED,
+ PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
+ };
+ Some(lsp::FileEvent {
+ uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
+ typ,
})
- .collect(),
- };
+ })
+ .collect(),
+ };
- if !params.changes.is_empty() {
- server
- .notify::<lsp::notification::DidChangeWatchedFiles>(params)
- .log_err();
- }
+ if !params.changes.is_empty() {
+ server
+ .notify::<lsp::notification::DidChangeWatchedFiles>(params)
+ .log_err();
}
}
}
@@ -6581,6 +6709,68 @@ impl Project {
Ok(proto::OnTypeFormattingResponse { transaction })
}
+ async fn handle_inlay_hints(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::InlayHints>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::InlayHintsResponse> {
+ let sender_id = envelope.original_sender_id()?;
+ let buffer = this.update(&mut cx, |this, cx| {
+ this.opened_buffers
+ .get(&envelope.payload.buffer_id)
+ .and_then(|buffer| buffer.upgrade(cx))
+ .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
+ })?;
+ let buffer_version = deserialize_version(&envelope.payload.version);
+
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(buffer_version.clone())
+ })
+ .await
+ .with_context(|| {
+ format!(
+ "waiting for version {:?} for buffer {}",
+ buffer_version,
+ buffer.id()
+ )
+ })?;
+
+ let start = envelope
+ .payload
+ .start
+ .and_then(deserialize_anchor)
+ .context("missing range start")?;
+ let end = envelope
+ .payload
+ .end
+ .and_then(deserialize_anchor)
+ .context("missing range end")?;
+ let buffer_hints = this
+ .update(&mut cx, |project, cx| {
+ project.inlay_hints(buffer, start..end, cx)
+ })
+ .await
+ .context("inlay hints fetch")?;
+
+ Ok(this.update(&mut cx, |project, cx| {
+ InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx)
+ }))
+ }
+
+ async fn handle_refresh_inlay_hints(
+ this: ModelHandle<Self>,
+ _: TypedEnvelope<proto::RefreshInlayHints>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::Ack> {
+ this.update(&mut cx, |_, cx| {
+ cx.emit(Event::RefreshInlays);
+ });
+ Ok(proto::Ack {})
+ }
+
async fn handle_lsp_command<T: LspCommand>(
this: ModelHandle<Self>,
envelope: TypedEnvelope<T::ProtoRequest>,
@@ -7316,16 +7506,11 @@ impl Project {
) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
self.language_server_ids_for_buffer(buffer, cx)
.into_iter()
- .filter_map(|server_id| {
- let server = self.language_servers.get(&server_id)?;
- if let LanguageServerState::Running {
+ .filter_map(|server_id| match self.language_servers.get(&server_id)? {
+ LanguageServerState::Running {
adapter, server, ..
- } = server
- {
- Some((adapter, server))
- } else {
- None
- }
+ } => Some((adapter, server)),
+ _ => None,
})
}
@@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope {
}
}
+impl From<String> for Rope {
+ fn from(text: String) -> Self {
+ Rope::from(text.as_str())
+ }
+}
+
impl fmt::Display for Rope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for chunk in self.chunks() {
@@ -136,6 +136,10 @@ message Envelope {
OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
+
+ InlayHints inlay_hints = 116;
+ InlayHintsResponse inlay_hints_response = 117;
+ RefreshInlayHints refresh_inlay_hints = 118;
}
}
@@ -705,6 +709,68 @@ message OnTypeFormattingResponse {
Transaction transaction = 1;
}
+message InlayHints {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ Anchor start = 3;
+ Anchor end = 4;
+ repeated VectorClockEntry version = 5;
+}
+
+message InlayHintsResponse {
+ repeated InlayHint hints = 1;
+ repeated VectorClockEntry version = 2;
+}
+
+message InlayHint {
+ Anchor position = 1;
+ InlayHintLabel label = 2;
+ optional string kind = 3;
+ bool padding_left = 4;
+ bool padding_right = 5;
+ InlayHintTooltip tooltip = 6;
+}
+
+message InlayHintLabel {
+ oneof label {
+ string value = 1;
+ InlayHintLabelParts label_parts = 2;
+ }
+}
+
+message InlayHintLabelParts {
+ repeated InlayHintLabelPart parts = 1;
+}
+
+message InlayHintLabelPart {
+ string value = 1;
+ InlayHintLabelPartTooltip tooltip = 2;
+ Location location = 3;
+}
+
+message InlayHintTooltip {
+ oneof content {
+ string value = 1;
+ MarkupContent markup_content = 2;
+ }
+}
+
+message InlayHintLabelPartTooltip {
+ oneof content {
+ string value = 1;
+ MarkupContent markup_content = 2;
+ }
+}
+
+message RefreshInlayHints {
+ uint64 project_id = 1;
+}
+
+message MarkupContent {
+ string kind = 1;
+ string value = 2;
+}
+
message PerformRenameResponse {
ProjectTransaction transaction = 2;
}
@@ -198,6 +198,9 @@ messages!(
(PerformRenameResponse, Background),
(OnTypeFormatting, Background),
(OnTypeFormattingResponse, Background),
+ (InlayHints, Background),
+ (InlayHintsResponse, Background),
+ (RefreshInlayHints, Foreground),
(Ping, Foreground),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
@@ -286,6 +289,8 @@ request_messages!(
(PerformRename, PerformRenameResponse),
(PrepareRename, PrepareRenameResponse),
(OnTypeFormatting, OnTypeFormattingResponse),
+ (InlayHints, InlayHintsResponse),
+ (RefreshInlayHints, Ack),
(ReloadBuffers, ReloadBuffersResponse),
(RequestContact, Ack),
(RemoveContact, Ack),
@@ -332,6 +337,8 @@ entity_messages!(
OpenBufferForSymbol,
PerformRename,
OnTypeFormatting,
+ InlayHints,
+ RefreshInlayHints,
PrepareRename,
ReloadBuffers,
RemoveProjectCollaborator,
@@ -97,6 +97,42 @@ where
}
}
+ pub fn next_item(&self) -> Option<&'a T> {
+ self.assert_did_seek();
+ if let Some(entry) = self.stack.last() {
+ if entry.index == entry.tree.0.items().len() - 1 {
+ if let Some(next_leaf) = self.next_leaf() {
+ Some(next_leaf.0.items().first().unwrap())
+ } else {
+ None
+ }
+ } else {
+ match *entry.tree.0 {
+ Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]),
+ _ => unreachable!(),
+ }
+ }
+ } else if self.at_end {
+ None
+ } else {
+ self.tree.first()
+ }
+ }
+
+ fn next_leaf(&self) -> Option<&'a SumTree<T>> {
+ for entry in self.stack.iter().rev().skip(1) {
+ if entry.index < entry.tree.0.child_trees().len() - 1 {
+ match *entry.tree.0 {
+ Node::Internal {
+ ref child_trees, ..
+ } => return Some(child_trees[entry.index + 1].leftmost_leaf()),
+ Node::Leaf { .. } => unreachable!(),
+ };
+ }
+ }
+ None
+ }
+
pub fn prev_item(&self) -> Option<&'a T> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
@@ -95,31 +95,18 @@ impl<D> fmt::Debug for End<D> {
}
}
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)]
pub enum Bias {
+ #[default]
Left,
Right,
}
-impl Default for Bias {
- fn default() -> Self {
- Bias::Left
- }
-}
-
-impl PartialOrd for Bias {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for Bias {
- fn cmp(&self, other: &Self) -> Ordering {
- match (self, other) {
- (Self::Left, Self::Left) => Ordering::Equal,
- (Self::Left, Self::Right) => Ordering::Less,
- (Self::Right, Self::Right) => Ordering::Equal,
- (Self::Right, Self::Left) => Ordering::Greater,
+impl Bias {
+ pub fn invert(self) -> Self {
+ match self {
+ Self::Left => Self::Right,
+ Self::Right => Self::Left,
}
}
}
@@ -838,6 +825,14 @@ mod tests {
assert_eq!(cursor.item(), None);
}
+ if before_start {
+ assert_eq!(cursor.next_item(), reference_items.get(0));
+ } else if pos + 1 < reference_items.len() {
+ assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]);
+ } else {
+ assert_eq!(cursor.next_item(), None);
+ }
+
if i < 5 {
cursor.next(&());
if pos < reference_items.len() {
@@ -883,14 +878,17 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.prev(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
// Single-element tree
@@ -903,22 +901,26 @@ mod tests {
);
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 0);
let mut cursor = tree.cursor::<IntegersSummary>();
assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
cursor.seek(&Count(0), Bias::Right, &());
@@ -930,6 +932,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 1);
// Multiple-element tree
@@ -940,67 +943,80 @@ mod tests {
assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]);
assert_eq!(cursor.item(), Some(&3));
assert_eq!(cursor.prev_item(), Some(&2));
+ assert_eq!(cursor.next_item(), Some(&4));
assert_eq!(cursor.start().sum, 3);
cursor.next(&());
assert_eq!(cursor.item(), Some(&4));
assert_eq!(cursor.prev_item(), Some(&3));
+ assert_eq!(cursor.next_item(), Some(&5));
assert_eq!(cursor.start().sum, 6);
cursor.next(&());
assert_eq!(cursor.item(), Some(&5));
assert_eq!(cursor.prev_item(), Some(&4));
+ assert_eq!(cursor.next_item(), Some(&6));
assert_eq!(cursor.start().sum, 10);
cursor.next(&());
assert_eq!(cursor.item(), Some(&6));
assert_eq!(cursor.prev_item(), Some(&5));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 15);
cursor.next(&());
cursor.next(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&6));
assert_eq!(cursor.prev_item(), Some(&5));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 15);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&5));
assert_eq!(cursor.prev_item(), Some(&4));
+ assert_eq!(cursor.next_item(), Some(&6));
assert_eq!(cursor.start().sum, 10);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&4));
assert_eq!(cursor.prev_item(), Some(&3));
+ assert_eq!(cursor.next_item(), Some(&5));
assert_eq!(cursor.start().sum, 6);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&3));
assert_eq!(cursor.prev_item(), Some(&2));
+ assert_eq!(cursor.next_item(), Some(&4));
assert_eq!(cursor.start().sum, 3);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&2));
assert_eq!(cursor.prev_item(), Some(&1));
+ assert_eq!(cursor.next_item(), Some(&3));
assert_eq!(cursor.start().sum, 1);
cursor.prev(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&2));
assert_eq!(cursor.start().sum, 0);
cursor.prev(&());
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&1));
assert_eq!(cursor.start().sum, 0);
cursor.next(&());
assert_eq!(cursor.item(), Some(&1));
assert_eq!(cursor.prev_item(), None);
+ assert_eq!(cursor.next_item(), Some(&2));
assert_eq!(cursor.start().sum, 0);
let mut cursor = tree.cursor::<IntegersSummary>();
@@ -1012,6 +1028,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
cursor.seek(&Count(3), Bias::Right, &());
@@ -1023,6 +1040,7 @@ mod tests {
);
assert_eq!(cursor.item(), None);
assert_eq!(cursor.prev_item(), Some(&6));
+ assert_eq!(cursor.next_item(), None);
assert_eq!(cursor.start().sum, 21);
// Seeking can bias left or right
@@ -689,6 +689,7 @@ pub struct Editor {
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,
pub syntax: Arc<SyntaxTheme>,
+ pub hint: HighlightStyle,
pub suggestion: HighlightStyle,
pub diagnostic_path_header: DiagnosticPathHeader,
pub diagnostic_header: DiagnosticHeader,
@@ -53,6 +53,7 @@ export default function editor(theme: ColorScheme): any {
active_line_background: with_opacity(background(layer, "on"), 0.75),
highlighted_line_background: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
+ hint: syntax.hint,
suggestion: syntax.predictive,
code_actions: {
indicator: toggleable({
@@ -17,6 +17,7 @@ export interface Syntax {
"comment.doc": SyntaxHighlightStyle
primary: SyntaxHighlightStyle
predictive: SyntaxHighlightStyle
+ hint: SyntaxHighlightStyle
// === Formatted Text ====== /
emphasis: SyntaxHighlightStyle
@@ -146,12 +147,23 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
"lch"
)
.hex()
+ // Mix the neutral and green colors to get a
+ // hint color distinct from any other color in the theme
+ const hint = chroma
+ .mix(
+ color_scheme.ramps.neutral(0.6).hex(),
+ color_scheme.ramps.blue(0.4).hex(),
+ 0.45,
+ "lch"
+ )
+ .hex()
const color = {
primary: color_scheme.ramps.neutral(1).hex(),
comment: color_scheme.ramps.neutral(0.71).hex(),
punctuation: color_scheme.ramps.neutral(0.86).hex(),
predictive: predictive,
+ hint: hint,
emphasis: color_scheme.ramps.blue(0.5).hex(),
string: color_scheme.ramps.orange(0.5).hex(),
function: color_scheme.ramps.yellow(0.5).hex(),
@@ -183,6 +195,10 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
color: color.predictive,
italic: true,
},
+ hint: {
+ color: color.hint,
+ weight: font_weights.bold,
+ },
emphasis: {
color: color.emphasis,
},