Support basic inlay hints (#2660)

Part of https://github.com/zed-industries/community/issues/138
Part of https://linear.app/zed-industries/issue/Z-477/inlay-hints

Supports LSP requests for inlay hints, LSP /refresh request to reload
them.
Reworks DisplayMap and underlying layer to unite suggestions with inlay
hints into new, `InlayMap`.
Adds a hint cache inside `Editor` that tracks buffer/project/LSP request
events, updates the hints and ensures opened editors are showing up to
date text hints on top.

Things left to do after this PR:
* docs on how to configure inlay hints
* blogpost
* dynamic hints: resolve, hover, navigation on click, etc.

Release Notes:

- Added basic support of inlay hints
This commit is contained in:
Kirill Bulatov 2023-06-29 22:49:39 +03:00 committed by GitHub
commit 167dd1c5d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 6069 additions and 1696 deletions

View file

@ -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,

View file

@ -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

View file

@ -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
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}
}

View file

@ -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;
}

View file

@ -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!(

View file

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

View file

@ -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) {

File diff suppressed because it is too large Load diff

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

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

View file

@ -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
}
}

View file

@ -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,
})
}

View file

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

View file

@ -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;
}

View file

@ -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,

View file

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

View file

@ -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

View file

@ -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,

View file

View file

@ -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({

View file

@ -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,
},