Skip to content

Commit a1cc587

Browse files
SomeoneToIgnoreVeykrildinocosta
authored andcommitted
Rework inlay hints system (zed-industries#40183)
Closes zed-industries#40047 Closes zed-industries#24798 Closes zed-industries#24788 Before, each editor, even if it's the same buffer split in 2, was querying for inlay hints separately, and storing the whole inlay hint twice, in `Editor`'s `display_map` and its `inlay_hint_cache` fields. Now, instead of `inlay_hint_cache`, each editor maintains a minimal set of metadata (which area was queried by what task) instead, and all LSP inlay hint data had been moved into `LspStore`, both local and remote flavors store the data. This allows Zed, as long as a buffer is open, to reuse the inlay hint data similar to how document colors and code lens are now stored and reused. Unlike other reused LSP data, inlay hints data is the first one that's possible to query by document ranges and previous version had issue with caching and invalidating such ranges already queried for. The new version re-approaches this by chunking the file into row ranges, which are queried based on the editors' visible area. Among the corresponding refactoring, one notable difference in inlays display are multi buffers: buffers in them are not [registered](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen) in the language server until a caret/selection is placed inside their excerpts inside the multi buffer. New inlays code does not query language servers for unregistered buffers, as servers usually respond with empty responses or errors in such cases. Release Notes: - Reworked inlay hints to be less error-prone --------- Co-authored-by: Lukas Wirth <[email protected]> Co-authored-by: dino <[email protected]> Co-authored-by: Lukas Wirth <[email protected]>
1 parent 261a423 commit a1cc587

File tree

31 files changed

+3286
-2613
lines changed

31 files changed

+3286
-2613
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent_ui/src/acp/message_editor.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
1111
use collections::{HashMap, HashSet};
1212
use editor::{
1313
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
14-
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
14+
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
1515
MultiBuffer, ToOffset,
1616
actions::Paste,
17-
display_map::{Crease, CreaseId, FoldId, Inlay},
17+
display_map::{Crease, CreaseId, FoldId},
1818
};
1919
use futures::{
2020
FutureExt as _,
@@ -29,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind};
2929
use language_model::LanguageModelImage;
3030
use postage::stream::Stream as _;
3131
use project::{
32-
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
32+
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
33+
Worktree,
3334
};
3435
use prompt_store::{PromptId, PromptStore};
3536
use rope::Point;
@@ -75,7 +76,7 @@ pub enum MessageEditorEvent {
7576

7677
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
7778

78-
const COMMAND_HINT_INLAY_ID: u32 = 0;
79+
const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
7980

8081
impl MessageEditor {
8182
pub fn new(
@@ -151,7 +152,7 @@ impl MessageEditor {
151152
let has_new_hint = !new_hints.is_empty();
152153
editor.splice_inlays(
153154
if has_hint {
154-
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
155+
&[COMMAND_HINT_INLAY_ID]
155156
} else {
156157
&[]
157158
},

crates/collab/src/rpc.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,6 @@ impl Server {
343343
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
344344
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
345345
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
346-
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
347346
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
348347
.add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
349348
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)

crates/collab/src/tests/editor_tests.rs

Lines changed: 86 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update(
18491849
..lsp::ServerCapabilities::default()
18501850
};
18511851
client_a.language_registry().add(rust_lang());
1852+
1853+
// Set up the language server to return an additional inlay hint on each request.
1854+
let edits_made = Arc::new(AtomicUsize::new(0));
1855+
let closure_edits_made = Arc::clone(&edits_made);
18521856
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
18531857
"Rust",
18541858
FakeLspAdapter {
18551859
capabilities: capabilities.clone(),
1860+
initializer: Some(Box::new(move |fake_language_server| {
1861+
let closure_edits_made = closure_edits_made.clone();
1862+
fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1863+
move |params, _| {
1864+
let edits_made_2 = Arc::clone(&closure_edits_made);
1865+
async move {
1866+
assert_eq!(
1867+
params.text_document.uri,
1868+
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1869+
);
1870+
let edits_made =
1871+
AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
1872+
Ok(Some(vec![lsp::InlayHint {
1873+
position: lsp::Position::new(0, edits_made as u32),
1874+
label: lsp::InlayHintLabel::String(edits_made.to_string()),
1875+
kind: None,
1876+
text_edits: None,
1877+
tooltip: None,
1878+
padding_left: None,
1879+
padding_right: None,
1880+
data: None,
1881+
}]))
1882+
}
1883+
},
1884+
);
1885+
})),
18561886
..FakeLspAdapter::default()
18571887
},
18581888
);
@@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update(
18941924
.unwrap();
18951925

18961926
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
1897-
executor.start_waiting();
18981927

18991928
// The host opens a rust file.
1900-
let _buffer_a = project_a
1901-
.update(cx_a, |project, cx| {
1902-
project.open_local_buffer(path!("/a/main.rs"), cx)
1903-
})
1904-
.await
1905-
.unwrap();
1906-
let editor_a = workspace_a
1907-
.update_in(cx_a, |workspace, window, cx| {
1908-
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1909-
})
1910-
.await
1911-
.unwrap()
1912-
.downcast::<Editor>()
1913-
.unwrap();
1914-
1929+
let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
1930+
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1931+
});
19151932
let fake_language_server = fake_language_servers.next().await.unwrap();
1916-
1917-
// Set up the language server to return an additional inlay hint on each request.
1918-
let edits_made = Arc::new(AtomicUsize::new(0));
1919-
let closure_edits_made = Arc::clone(&edits_made);
1920-
fake_language_server
1921-
.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1922-
let edits_made_2 = Arc::clone(&closure_edits_made);
1923-
async move {
1924-
assert_eq!(
1925-
params.text_document.uri,
1926-
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1927-
);
1928-
let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
1929-
Ok(Some(vec![lsp::InlayHint {
1930-
position: lsp::Position::new(0, edits_made as u32),
1931-
label: lsp::InlayHintLabel::String(edits_made.to_string()),
1932-
kind: None,
1933-
text_edits: None,
1934-
tooltip: None,
1935-
padding_left: None,
1936-
padding_right: None,
1937-
data: None,
1938-
}]))
1939-
}
1940-
})
1941-
.next()
1942-
.await
1943-
.unwrap();
1944-
1933+
let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
19451934
executor.run_until_parked();
19461935

19471936
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
1948-
editor_a.update(cx_a, |editor, _| {
1937+
editor_a.update(cx_a, |editor, cx| {
19491938
assert_eq!(
19501939
vec![initial_edit.to_string()],
1951-
extract_hint_labels(editor),
1940+
extract_hint_labels(editor, cx),
19521941
"Host should get its first hints when opens an editor"
19531942
);
19541943
});
@@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
19631952
.unwrap();
19641953

19651954
executor.run_until_parked();
1966-
editor_b.update(cx_b, |editor, _| {
1955+
editor_b.update(cx_b, |editor, cx| {
19671956
assert_eq!(
19681957
vec![initial_edit.to_string()],
1969-
extract_hint_labels(editor),
1958+
extract_hint_labels(editor, cx),
19701959
"Client should get its first hints when opens an editor"
19711960
);
19721961
});
@@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
19811970
cx_b.focus(&editor_b);
19821971

19831972
executor.run_until_parked();
1984-
editor_a.update(cx_a, |editor, _| {
1973+
editor_a.update(cx_a, |editor, cx| {
19851974
assert_eq!(
19861975
vec![after_client_edit.to_string()],
1987-
extract_hint_labels(editor),
1976+
extract_hint_labels(editor, cx),
19881977
);
19891978
});
1990-
editor_b.update(cx_b, |editor, _| {
1979+
editor_b.update(cx_b, |editor, cx| {
19911980
assert_eq!(
19921981
vec![after_client_edit.to_string()],
1993-
extract_hint_labels(editor),
1982+
extract_hint_labels(editor, cx),
19941983
);
19951984
});
19961985

@@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
20041993
cx_a.focus(&editor_a);
20051994

20061995
executor.run_until_parked();
2007-
editor_a.update(cx_a, |editor, _| {
1996+
editor_a.update(cx_a, |editor, cx| {
20081997
assert_eq!(
20091998
vec![after_host_edit.to_string()],
2010-
extract_hint_labels(editor),
1999+
extract_hint_labels(editor, cx),
20112000
);
20122001
});
2013-
editor_b.update(cx_b, |editor, _| {
2002+
editor_b.update(cx_b, |editor, cx| {
20142003
assert_eq!(
20152004
vec![after_host_edit.to_string()],
2016-
extract_hint_labels(editor),
2005+
extract_hint_labels(editor, cx),
20172006
);
20182007
});
20192008

@@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update(
20252014
.expect("inlay refresh request failed");
20262015

20272016
executor.run_until_parked();
2028-
editor_a.update(cx_a, |editor, _| {
2017+
editor_a.update(cx_a, |editor, cx| {
20292018
assert_eq!(
20302019
vec![after_special_edit_for_refresh.to_string()],
2031-
extract_hint_labels(editor),
2020+
extract_hint_labels(editor, cx),
20322021
"Host should react to /refresh LSP request"
20332022
);
20342023
});
2035-
editor_b.update(cx_b, |editor, _| {
2024+
editor_b.update(cx_b, |editor, cx| {
20362025
assert_eq!(
20372026
vec![after_special_edit_for_refresh.to_string()],
2038-
extract_hint_labels(editor),
2027+
extract_hint_labels(editor, cx),
20392028
"Guest should get a /refresh LSP request propagated by host"
20402029
);
20412030
});
20422031
}
20432032

2044-
// This test started hanging on seed 2 after the theme settings
2045-
// PR. The hypothesis is that it's been buggy for a while, but got lucky
2046-
// on seeds.
2047-
#[ignore]
20482033
#[gpui::test(iterations = 10)]
20492034
async fn test_inlay_hint_refresh_is_forwarded(
20502035
cx_a: &mut TestAppContext,
@@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
22062191
executor.finish_waiting();
22072192

22082193
executor.run_until_parked();
2209-
editor_a.update(cx_a, |editor, _| {
2194+
editor_a.update(cx_a, |editor, cx| {
22102195
assert!(
2211-
extract_hint_labels(editor).is_empty(),
2196+
extract_hint_labels(editor, cx).is_empty(),
22122197
"Host should get no hints due to them turned off"
22132198
);
22142199
});
22152200

22162201
executor.run_until_parked();
2217-
editor_b.update(cx_b, |editor, _| {
2202+
editor_b.update(cx_b, |editor, cx| {
22182203
assert_eq!(
22192204
vec!["initial hint".to_string()],
2220-
extract_hint_labels(editor),
2205+
extract_hint_labels(editor, cx),
22212206
"Client should get its first hints when opens an editor"
22222207
);
22232208
});
@@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
22292214
.into_response()
22302215
.expect("inlay refresh request failed");
22312216
executor.run_until_parked();
2232-
editor_a.update(cx_a, |editor, _| {
2217+
editor_a.update(cx_a, |editor, cx| {
22332218
assert!(
2234-
extract_hint_labels(editor).is_empty(),
2219+
extract_hint_labels(editor, cx).is_empty(),
22352220
"Host should get no hints due to them turned off, even after the /refresh"
22362221
);
22372222
});
22382223

22392224
executor.run_until_parked();
2240-
editor_b.update(cx_b, |editor, _| {
2225+
editor_b.update(cx_b, |editor, cx| {
22412226
assert_eq!(
22422227
vec!["other hint".to_string()],
2243-
extract_hint_labels(editor),
2228+
extract_hint_labels(editor, cx),
22442229
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
22452230
);
22462231
});
@@ -4217,15 +4202,35 @@ fn tab_undo_assert(
42174202
cx_b.assert_editor_state(expected_initial);
42184203
}
42194204

4220-
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
4221-
let mut labels = Vec::new();
4222-
for hint in editor.inlay_hint_cache().hints() {
4223-
match hint.label {
4224-
project::InlayHintLabel::String(s) => labels.push(s),
4225-
_ => unreachable!(),
4226-
}
4205+
fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4206+
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4207+
4208+
let mut all_cached_labels = Vec::new();
4209+
let mut all_fetched_hints = Vec::new();
4210+
for buffer in editor.buffer().read(cx).all_buffers() {
4211+
lsp_store.update(cx, |lsp_store, cx| {
4212+
let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4213+
all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4214+
let mut label = hint.text().to_string();
4215+
if hint.padding_left {
4216+
label.insert(0, ' ');
4217+
}
4218+
if hint.padding_right {
4219+
label.push_str(" ");
4220+
}
4221+
label
4222+
}));
4223+
all_fetched_hints.extend(hints.all_fetched_hints());
4224+
});
42274225
}
4228-
labels
4226+
4227+
assert!(
4228+
all_fetched_hints.is_empty(),
4229+
"Did not expect background hints fetch tasks, but got {} of them",
4230+
all_fetched_hints.len()
4231+
);
4232+
4233+
all_cached_labels
42294234
}
42304235

42314236
#[track_caller]

crates/diagnostics/src/diagnostics_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use super::*;
22
use collections::{HashMap, HashSet};
33
use editor::{
4-
DisplayPoint, EditorSettings,
4+
DisplayPoint, EditorSettings, Inlay,
55
actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
6-
display_map::{DisplayRow, Inlay},
6+
display_map::DisplayRow,
77
test::{
88
editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
99
editor_test_context::EditorTestContext,

0 commit comments

Comments
 (0)