Rework inlay hints system (#40183)

Closes https://github.com/zed-industries/zed/issues/40047
Closes https://github.com/zed-industries/zed/issues/24798
Closes https://github.com/zed-industries/zed/issues/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 <lukas@zed.dev>
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Kirill Bulatov
2025-10-22 22:34:15 +03:00
committed by GitHub
co-authored by Lukas Wirth dino Lukas Wirth
parent 5738bde3ce
commit ed5b9a4705
31 changed files with 3298 additions and 2625 deletions
Generated
+1
View File
@@ -14955,6 +14955,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
"lsp",
"menu",
"project",
"schemars 1.0.4",
+6 -5
View File
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
use collections::{HashMap, HashSet};
use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
MultiBuffer, ToOffset,
actions::Paste,
display_map::{Crease, CreaseId, FoldId, Inlay},
display_map::{Crease, CreaseId, FoldId},
};
use futures::{
FutureExt as _,
@@ -29,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage;
use postage::stream::Stream as _;
use project::{
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
Worktree,
};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
@@ -75,7 +76,7 @@ pub enum MessageEditorEvent {
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
const COMMAND_HINT_INLAY_ID: u32 = 0;
const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
impl MessageEditor {
pub fn new(
@@ -151,7 +152,7 @@ impl MessageEditor {
let has_new_hint = !new_hints.is_empty();
editor.splice_inlays(
if has_hint {
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
&[COMMAND_HINT_INLAY_ID]
} else {
&[]
},
-1
View File
@@ -343,7 +343,6 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
.add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
+86 -81
View File
@@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update(
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
// Set up the language server to return an additional inlay hint on each request.
let edits_made = Arc::new(AtomicUsize::new(0));
let closure_edits_made = Arc::clone(&edits_made);
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
initializer: Some(Box::new(move |fake_language_server| {
let closure_edits_made = closure_edits_made.clone();
fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
move |params, _| {
let edits_made_2 = Arc::clone(&closure_edits_made);
async move {
assert_eq!(
params.text_document.uri,
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made =
AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, edits_made as u32),
label: lsp::InlayHintLabel::String(edits_made.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
},
);
})),
..FakeLspAdapter::default()
},
);
@@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.unwrap();
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
executor.start_waiting();
// The host opens a rust file.
let _buffer_a = project_a
.update(cx_a, |project, cx| {
project.open_local_buffer(path!("/a/main.rs"), cx)
})
.await
.unwrap();
let editor_a = workspace_a
.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
// Set up the language server to return an additional inlay hint on each request.
let edits_made = Arc::new(AtomicUsize::new(0));
let closure_edits_made = Arc::clone(&edits_made);
fake_language_server
.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let edits_made_2 = Arc::clone(&closure_edits_made);
async move {
assert_eq!(
params.text_document.uri,
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, edits_made as u32),
label: lsp::InlayHintLabel::String(edits_made.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()
.await
.unwrap();
let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
executor.run_until_parked();
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
vec![initial_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Host should get its first hints when opens an editor"
);
});
@@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.unwrap();
executor.run_until_parked();
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec![initial_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Client should get its first hints when opens an editor"
);
});
@@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx_b.focus(&editor_b);
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
vec![after_client_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
);
});
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec![after_client_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
);
});
@@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx_a.focus(&editor_a);
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
vec![after_host_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
);
});
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec![after_host_edit.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
);
});
@@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.expect("inlay refresh request failed");
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
vec![after_special_edit_for_refresh.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Host should react to /refresh LSP request"
);
});
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec![after_special_edit_for_refresh.to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Guest should get a /refresh LSP request propagated by host"
);
});
}
// This test started hanging on seed 2 after the theme settings
// PR. The hypothesis is that it's been buggy for a while, but got lucky
// on seeds.
#[ignore]
#[gpui::test(iterations = 10)]
async fn test_inlay_hint_refresh_is_forwarded(
cx_a: &mut TestAppContext,
@@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
executor.finish_waiting();
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert!(
extract_hint_labels(editor).is_empty(),
extract_hint_labels(editor, cx).is_empty(),
"Host should get no hints due to them turned off"
);
});
executor.run_until_parked();
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec!["initial hint".to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Client should get its first hints when opens an editor"
);
});
@@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
.into_response()
.expect("inlay refresh request failed");
executor.run_until_parked();
editor_a.update(cx_a, |editor, _| {
editor_a.update(cx_a, |editor, cx| {
assert!(
extract_hint_labels(editor).is_empty(),
extract_hint_labels(editor, cx).is_empty(),
"Host should get no hints due to them turned off, even after the /refresh"
);
});
executor.run_until_parked();
editor_b.update(cx_b, |editor, _| {
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
vec!["other hint".to_string()],
extract_hint_labels(editor),
extract_hint_labels(editor, cx),
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
);
});
@@ -4217,15 +4202,35 @@ fn tab_undo_assert(
cx_b.assert_editor_state(expected_initial);
}
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new();
for hint in editor.inlay_hint_cache().hints() {
match hint.label {
project::InlayHintLabel::String(s) => labels.push(s),
_ => unreachable!(),
}
fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
let mut all_cached_labels = Vec::new();
let mut all_fetched_hints = Vec::new();
for buffer in editor.buffer().read(cx).all_buffers() {
lsp_store.update(cx, |lsp_store, cx| {
let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
let mut label = hint.text().to_string();
if hint.padding_left {
label.insert(0, ' ');
}
if hint.padding_right {
label.push_str(" ");
}
label
}));
all_fetched_hints.extend(hints.all_fetched_hints());
});
}
labels
assert!(
all_fetched_hints.is_empty(),
"Did not expect background hints fetch tasks, but got {} of them",
all_fetched_hints.len()
);
all_cached_labels
}
#[track_caller]
+2 -2
View File
@@ -1,9 +1,9 @@
use super::*;
use collections::{HashMap, HashSet};
use editor::{
DisplayPoint, EditorSettings,
DisplayPoint, EditorSettings, Inlay,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
display_map::{DisplayRow, Inlay},
display_map::DisplayRow,
test::{
editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext,
+4 -23
View File
@@ -27,7 +27,7 @@ mod tab_map;
mod wrap_map;
use crate::{
EditorStyle, InlayId, RowExt, hover_links::InlayHighlight, movement::TextLayoutDetails,
EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails,
};
pub use block_map::{
Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
@@ -42,7 +42,6 @@ pub use fold_map::{
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
use inlay_map::InlaySnapshot;
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::{is_invisible, replacement};
@@ -50,9 +49,10 @@ use language::{
OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings,
};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
RowInfo, ToOffset, ToPoint,
};
use project::InlayId;
use project::project_settings::DiagnosticSeverity;
use serde::Deserialize;
@@ -594,25 +594,6 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn remove_inlays_for_excerpts(
&mut self,
excerpts_removed: &[ExcerptId],
cx: &mut Context<Self>,
) {
let to_remove = self
.inlay_map
.current_inlays()
.filter_map(|inlay| {
if excerpts_removed.contains(&inlay.position.excerpt_id) {
Some(inlay.id)
} else {
None
}
})
.collect::<Vec<_>>();
self.splice_inlays(&to_remove, Vec::new(), cx);
}
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
let language = buffer
+2 -1
View File
@@ -1,4 +1,4 @@
use crate::{InlayId, display_map::inlay_map::InlayChunk};
use crate::display_map::inlay_map::InlayChunk;
use super::{
Highlights,
@@ -9,6 +9,7 @@ use language::{Edit, HighlightId, Point, TextSummary};
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
};
use project::InlayId;
use std::{
any::TypeId,
cmp::{self, Ordering},
+18 -95
View File
@@ -1,17 +1,18 @@
use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{
Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
use crate::{
ChunkRenderer, HighlightStyles,
inlays::{Inlay, InlayContent},
};
use collections::BTreeSet;
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset};
use project::InlayId;
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
sync::{Arc, OnceLock},
sync::Arc,
};
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
use text::{ChunkBitmaps, Patch, Rope};
use text::{ChunkBitmaps, Patch};
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
@@ -37,85 +38,6 @@ enum Transform {
Inlay(Inlay),
}
#[derive(Debug, Clone)]
pub struct Inlay {
pub id: InlayId,
pub position: Anchor,
pub content: InlayContent,
}
#[derive(Debug, Clone)]
pub enum InlayContent {
Text(text::Rope),
Color(Hsla),
}
impl Inlay {
pub fn hint(id: u32, position: Anchor, hint: &project::InlayHint) -> Self {
let mut text = hint.text();
if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
text.push(" ");
}
if hint.padding_left && text.chars_at(0).next() != Some(' ') {
text.push_front(" ");
}
Self {
id: InlayId::Hint(id),
position,
content: InlayContent::Text(text),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn mock_hint(id: u32, position: Anchor, text: impl Into<Rope>) -> Self {
Self {
id: InlayId::Hint(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn color(id: u32, position: Anchor, color: Rgba) -> Self {
Self {
id: InlayId::Color(id),
position,
content: InlayContent::Color(color.into()),
}
}
pub fn edit_prediction<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
Self {
id: InlayId::EditPrediction(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn debugger<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
Self {
id: InlayId::DebuggerValue(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn text(&self) -> &Rope {
static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
match &self.content {
InlayContent::Text(text) => text,
InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("")),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn get_color(&self) -> Option<Hsla> {
match self.content {
InlayContent::Color(color) => Some(color),
_ => None,
}
}
}
impl sum_tree::Item for Transform {
type Summary = TransformSummary;
@@ -750,7 +672,7 @@ impl InlayMap {
#[cfg(test)]
pub(crate) fn randomly_mutate(
&mut self,
next_inlay_id: &mut u32,
next_inlay_id: &mut usize,
rng: &mut rand::rngs::StdRng,
) -> (InlaySnapshot, Vec<InlayEdit>) {
use rand::prelude::*;
@@ -1245,17 +1167,18 @@ const fn is_utf8_char_boundary(byte: u8) -> bool {
mod tests {
use super::*;
use crate::{
InlayId, MultiBuffer,
MultiBuffer,
display_map::{HighlightKey, InlayHighlights, TextHighlights},
hover_links::InlayHighlight,
};
use gpui::{App, HighlightStyle};
use multi_buffer::Anchor;
use project::{InlayHint, InlayHintLabel, ResolveState};
use rand::prelude::*;
use settings::SettingsStore;
use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
use sum_tree::TreeMap;
use text::Patch;
use text::{Patch, Rope};
use util::RandomCharIter;
use util::post_inc;
@@ -1263,7 +1186,7 @@ mod tests {
fn test_inlay_properties_label_padding() {
assert_eq!(
Inlay::hint(
0,
InlayId::Hint(0),
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
@@ -1283,7 +1206,7 @@ mod tests {
assert_eq!(
Inlay::hint(
0,
InlayId::Hint(0),
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
@@ -1303,7 +1226,7 @@ mod tests {
assert_eq!(
Inlay::hint(
0,
InlayId::Hint(0),
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
@@ -1323,7 +1246,7 @@ mod tests {
assert_eq!(
Inlay::hint(
0,
InlayId::Hint(0),
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
@@ -1346,7 +1269,7 @@ mod tests {
fn test_inlay_hint_padding_with_multibyte_chars() {
assert_eq!(
Inlay::hint(
0,
InlayId::Hint(0),
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("🎨".to_string()),
+110 -395
View File
@@ -7,7 +7,6 @@
//! * [`element`] — the place where all rendering happens
//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them.
//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.).
//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly.
//!
//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s).
//!
@@ -24,7 +23,7 @@ mod highlight_matching_bracket;
mod hover_links;
pub mod hover_popover;
mod indent_guides;
mod inlay_hint_cache;
mod inlays;
pub mod items;
mod jsx_tag_auto_close;
mod linked_editing_ranges;
@@ -61,6 +60,7 @@ pub use element::{
};
pub use git::blame::BlameRenderer;
pub use hover_popover::hover_markdown_style;
pub use inlays::Inlay;
pub use items::MAX_TAB_TITLE_LEN;
pub use lsp::CompletionContext;
pub use lsp_ext::lsp_tasks;
@@ -112,10 +112,10 @@ use gpui::{
div, point, prelude::*, pulsating_between, px, relative, size,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
use hover_links::{HoverLink, HoveredLinkState, find_file};
use hover_popover::{HoverState, hide_hover};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
use inlays::{InlaySplice, inlay_hints::InlayHintRefreshReason};
use itertools::{Either, Itertools};
use language::{
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
@@ -124,8 +124,8 @@ use language::{
IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
language_settings,
},
point_from_lsp, point_to_lsp, text_diff_with_options,
};
@@ -146,9 +146,9 @@ use parking_lot::Mutex;
use persistence::DB;
use project::{
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint,
Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath,
ProjectTransaction, TaskSourceKind,
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
ProjectPath, ProjectTransaction, TaskSourceKind,
debugger::{
breakpoint_store::{
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -157,7 +157,10 @@ use project::{
session::{Session, SessionEvent},
},
git_store::{GitStoreEvent, RepositoryEvent},
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
lsp_store::{
CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget,
OpenLspBufferHandle,
},
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
};
use rand::seq::SliceRandom;
@@ -178,7 +181,7 @@ use std::{
iter::{self, Peekable},
mem,
num::NonZeroU32,
ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
ops::{Deref, DerefMut, Not, Range, RangeInclusive},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
@@ -208,6 +211,10 @@ use crate::{
code_context_menus::CompletionsMenuSource,
editor_settings::MultiCursorModifier,
hover_links::{find_url, find_url_from_range},
inlays::{
InlineValueCache,
inlay_hints::{LspInlayHintData, inlay_hint_settings},
},
scroll::{ScrollOffset, ScrollPixelOffset},
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
};
@@ -261,42 +268,6 @@ impl ReportEditorEvent {
}
}
struct InlineValueCache {
enabled: bool,
inlays: Vec<InlayId>,
refresh_task: Task<Option<()>>,
}
impl InlineValueCache {
fn new(enabled: bool) -> Self {
Self {
enabled,
inlays: Vec::new(),
refresh_task: Task::ready(None),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
EditPrediction(u32),
DebuggerValue(u32),
// LSP
Hint(u32),
Color(u32),
}
impl InlayId {
fn id(&self) -> u32 {
match self {
Self::EditPrediction(id) => *id,
Self::DebuggerValue(id) => *id,
Self::Hint(id) => *id,
Self::Color(id) => *id,
}
}
}
pub enum ActiveDebugLine {}
pub enum DebugStackFrameLine {}
enum DocumentHighlightRead {}
@@ -1124,9 +1095,8 @@ pub struct Editor {
edit_prediction_preview: EditPredictionPreview,
edit_prediction_indent_conflict: bool,
edit_prediction_requires_modifier_in_indent_conflict: bool,
inlay_hint_cache: InlayHintCache,
next_inlay_id: u32,
next_color_inlay_id: u32,
next_inlay_id: usize,
next_color_inlay_id: usize,
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
gutter_dimensions: GutterDimensions,
@@ -1193,10 +1163,19 @@ pub struct Editor {
colors: Option<LspColorData>,
post_scroll_update: Task<()>,
refresh_colors_task: Task<()>,
inlay_hints: Option<LspInlayHintData>,
folding_newlines: Task<()>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
if debounce_ms > 0 {
Some(Duration::from_millis(debounce_ms))
} else {
None
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
enum NextScrollCursorCenterTopBottom {
#[default]
@@ -1621,31 +1600,6 @@ pub enum GotoDefinitionKind {
Implementation,
}
#[derive(Debug, Clone)]
enum InlayHintRefreshReason {
ModifiersChanged(bool),
Toggle(bool),
SettingsChange(InlayHintSettings),
NewLinesShown,
BufferEdited(HashSet<Arc<Language>>),
RefreshRequested,
ExcerptsRemoved(Vec<ExcerptId>),
}
impl InlayHintRefreshReason {
fn description(&self) -> &'static str {
match self {
Self::ModifiersChanged(_) => "modifiers changed",
Self::Toggle(_) => "toggle",
Self::SettingsChange(_) => "settings change",
Self::NewLinesShown => "new lines shown",
Self::BufferEdited(_) => "buffer edited",
Self::RefreshRequested => "refresh requested",
Self::ExcerptsRemoved(_) => "excerpts removed",
}
}
}
pub enum FormatTarget {
Buffers(HashSet<Entity<Buffer>>),
Ranges(Vec<Range<MultiBufferPoint>>),
@@ -1881,8 +1835,11 @@ impl Editor {
project::Event::RefreshCodeLens => {
// we always query lens with actions, without storing them, always refreshing them
}
project::Event::RefreshInlayHints => {
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
project::Event::RefreshInlayHints(server_id) => {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested(*server_id),
cx,
);
}
project::Event::LanguageServerRemoved(..) => {
if editor.tasks_update_task.is_none() {
@@ -1919,17 +1876,12 @@ impl Editor {
project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
let buffer_id = *buffer_id;
if editor.buffer().read(cx).buffer(buffer_id).is_some() {
let registered = editor.register_buffer(buffer_id, cx);
if registered {
editor.update_lsp_data(Some(buffer_id), window, cx);
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested,
cx,
);
refresh_linked_ranges(editor, window, cx);
editor.refresh_code_actions(window, cx);
editor.refresh_document_highlights(cx);
}
editor.register_buffer(buffer_id, cx);
editor.update_lsp_data(Some(buffer_id), window, cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
refresh_linked_ranges(editor, window, cx);
editor.refresh_code_actions(window, cx);
editor.refresh_document_highlights(cx);
}
}
@@ -2200,7 +2152,6 @@ impl Editor {
diagnostics_enabled: full_mode,
word_completions_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
@@ -2266,6 +2217,7 @@ impl Editor {
pull_diagnostics_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
inlay_hints: None,
next_color_inlay_id: 0,
post_scroll_update: Task::ready(()),
linked_edit_ranges: Default::default(),
@@ -2403,13 +2355,15 @@ impl Editor {
editor.go_to_active_debug_line(window, cx);
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
}
editor.minimap =
editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
editor.colors = Some(LspColorData::new(cx));
editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings));
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
}
editor.update_lsp_data(None, window, cx);
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
}
@@ -5198,179 +5152,8 @@ impl Editor {
}
}
pub fn toggle_inline_values(
&mut self,
_: &ToggleInlineValues,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
self.refresh_inline_values(cx);
}
pub fn toggle_inlay_hints(
&mut self,
_: &ToggleInlayHints,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.refresh_inlay_hints(
InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
cx,
);
}
pub fn inlay_hints_enabled(&self) -> bool {
self.inlay_hint_cache.enabled
}
pub fn inline_values_enabled(&self) -> bool {
self.inline_value_cache.enabled
}
#[cfg(any(test, feature = "test-support"))]
pub fn inline_value_inlays(&self, cx: &App) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
.cloned()
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn all_inlays(&self, cx: &App) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.cloned()
.collect()
}
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
if self.semantics_provider.is_none() || !self.mode.is_full() {
return;
}
let reason_description = reason.description();
let ignore_debounce = matches!(
reason,
InlayHintRefreshReason::SettingsChange(_)
| InlayHintRefreshReason::Toggle(_)
| InlayHintRefreshReason::ExcerptsRemoved(_)
| InlayHintRefreshReason::ModifiersChanged(_)
);
let (invalidate_cache, required_languages) = match reason {
InlayHintRefreshReason::ModifiersChanged(enabled) => {
match self.inlay_hint_cache.modifiers_override(enabled) {
Some(enabled) => {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.clear_inlay_hints(cx);
return;
}
}
None => return,
}
}
InlayHintRefreshReason::Toggle(enabled) => {
if self.inlay_hint_cache.toggle(enabled) {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.clear_inlay_hints(cx);
return;
}
} else {
return;
}
}
InlayHintRefreshReason::SettingsChange(new_settings) => {
match self.inlay_hint_cache.update_settings(
&self.buffer,
new_settings,
self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
cx,
) {
ControlFlow::Break(Some(InlaySplice {
to_remove,
to_insert,
})) => {
self.splice_inlays(&to_remove, to_insert, cx);
return;
}
ControlFlow::Break(None) => return,
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
}
}
InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
if let Some(InlaySplice {
to_remove,
to_insert,
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, cx| {
display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
});
return;
}
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
InlayHintRefreshReason::BufferEdited(buffer_languages) => {
(InvalidationStrategy::BufferEdited, Some(buffer_languages))
}
InlayHintRefreshReason::RefreshRequested => {
(InvalidationStrategy::RefreshRequested, None)
}
};
let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx);
visible_excerpts.retain(|_, (buffer, _, _)| {
self.registered_buffers
.contains_key(&buffer.read(cx).remote_id())
});
if let Some(InlaySplice {
to_remove,
to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh(
reason_description,
visible_excerpts,
invalidate_cache,
ignore_debounce,
cx,
) {
self.splice_inlays(&to_remove, to_insert, cx);
}
}
pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.map(|inlay| inlay.id)
.collect::<Vec<_>>(),
Vec::new(),
cx,
);
}
fn visible_inlay_hints<'a>(
&'a self,
cx: &'a Context<Editor>,
) -> impl Iterator<Item = &'a Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
}
pub fn visible_excerpts(
&self,
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
cx: &mut Context<Editor>,
) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
let Some(project) = self.project() else {
@@ -5389,9 +5172,8 @@ impl Editor {
+ 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_snapshot
.range_to_buffer_ranges(multi_buffer_visible_range)
.range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
@@ -5401,23 +5183,17 @@ impl Editor {
.read(cx)
.entry_for_id(buffer_file.project_entry_id()?)?;
if worktree_entry.is_ignored {
return None;
None
} else {
Some((
excerpt_id,
(
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer.version().clone(),
excerpt_visible_range,
),
))
}
let language = buffer.language()?;
if let Some(restrict_to_languages) = restrict_to_languages
&& !restrict_to_languages.contains(language)
{
return None;
}
Some((
excerpt_id,
(
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer.version().clone(),
excerpt_visible_range,
),
))
})
.collect()
}
@@ -5433,18 +5209,6 @@ impl Editor {
}
}
pub fn splice_inlays(
&self,
to_remove: &[InlayId],
to_insert: Vec<Inlay>,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |display_map, cx| {
display_map.splice_inlays(to_remove, to_insert, cx)
});
cx.notify();
}
fn trigger_on_type_formatting(
&self,
input: String,
@@ -17618,9 +17382,9 @@ impl Editor {
HashSet::default(),
cx,
);
cx.emit(project::Event::RefreshInlayHints);
});
});
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
}
@@ -20808,18 +20572,6 @@ impl Editor {
cx.notify();
}
pub(crate) fn highlight_inlays<T: 'static>(
&mut self,
highlights: Vec<InlayHighlight>,
style: HighlightStyle,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
map.highlight_inlays(TypeId::of::<T>(), highlights, style)
});
cx.notify();
}
pub fn text_highlights<'a, T: 'static>(
&'a self,
cx: &'a App,
@@ -20970,38 +20722,19 @@ impl Editor {
self.update_visible_edit_prediction(window, cx);
}
if let Some(edited_buffer) = edited_buffer {
if edited_buffer.read(cx).file().is_none() {
if let Some(buffer) = edited_buffer {
if buffer.read(cx).file().is_none() {
cx.emit(EditorEvent::TitleChanged);
}
let buffer_id = edited_buffer.read(cx).remote_id();
if let Some(project) = self.project.clone() {
if self.project.is_some() {
let buffer_id = buffer.read(cx).remote_id();
self.register_buffer(buffer_id, cx);
self.update_lsp_data(Some(buffer_id), window, cx);
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
multibuffer
.all_buffers()
.into_iter()
.filter_map(|buffer| {
buffer.update(cx, |buffer, cx| {
let language = buffer.language()?;
let should_discard = project.update(cx, |project, cx| {
project.is_local()
&& !project.has_language_servers_for(buffer, cx)
});
should_discard.not().then_some(language.clone())
})
})
.collect::<HashSet<_>>()
});
if !languages_affected.is_empty() {
self.refresh_inlay_hints(
InlayHintRefreshReason::BufferEdited(languages_affected),
cx,
);
}
self.refresh_inlay_hints(
InlayHintRefreshReason::BufferEdited(buffer_id),
cx,
);
}
}
@@ -21048,6 +20781,9 @@ impl Editor {
ids,
removed_buffer_ids,
} => {
if let Some(inlay_hints) = &mut self.inlay_hints {
inlay_hints.remove_inlay_chunk_data(removed_buffer_ids);
}
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
for buffer_id in removed_buffer_ids {
self.registered_buffers.remove(buffer_id);
@@ -21222,7 +20958,7 @@ impl Editor {
if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
}) {
if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() {
if !inlay_splice.is_empty() {
self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
}
self.refresh_colors_for_visible_range(None, window, cx);
@@ -21684,10 +21420,6 @@ impl Editor {
mouse_context_menu::deploy_context_menu(self, None, position, window, cx);
}
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
&self.inlay_hint_cache
}
pub fn replay_insert_event(
&mut self,
text: &str,
@@ -21726,21 +21458,6 @@ impl Editor {
self.handle_input(text, window, cx);
}
pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
let Some(provider) = self.semantics_provider.as_ref() else {
return false;
};
let mut supports = false;
self.buffer().update(cx, |this, cx| {
this.for_each_buffer(|buffer| {
supports |= provider.supports_inlay_hints(buffer, cx);
});
});
supports
}
pub fn is_focused(&self, window: &Window) -> bool {
self.focus_handle.is_focused(window)
}
@@ -22156,12 +21873,12 @@ impl Editor {
if self.ignore_lsp_data() {
return;
}
for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) {
for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
}
}
fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) -> bool {
fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
if !self.registered_buffers.contains_key(&buffer_id)
&& let Some(project) = self.project.as_ref()
{
@@ -22172,13 +21889,10 @@ impl Editor {
project.register_buffer_with_language_servers(&buffer, cx),
);
});
return true;
} else {
self.registered_buffers.remove(&buffer_id);
}
}
false
}
fn ignore_lsp_data(&self) -> bool {
@@ -22886,20 +22600,23 @@ pub trait SemanticsProvider {
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
fn applicable_inlay_chunks(
&self,
buffer_id: BufferId,
ranges: &[Range<text::Anchor>],
cx: &App,
) -> Vec<Range<BufferRow>>;
fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App);
fn inlay_hints(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
invalidate: InvalidationStrategy,
buffer: Entity<Buffer>,
ranges: Vec<Range<text::Anchor>>,
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
fn resolve_inlay_hint(
&self,
hint: InlayHint,
buffer_handle: Entity<Buffer>,
server_id: LanguageServerId,
cx: &mut App,
) -> Option<Task<anyhow::Result<InlayHint>>>;
) -> Option<HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>>>;
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool;
@@ -23392,26 +23109,34 @@ impl SemanticsProvider for Entity<Project> {
})
}
fn inlay_hints(
fn applicable_inlay_chunks(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
Some(self.update(cx, |project, cx| {
project.inlay_hints(buffer_handle, range, cx)
}))
buffer_id: BufferId,
ranges: &[Range<text::Anchor>],
cx: &App,
) -> Vec<Range<BufferRow>> {
self.read(cx)
.lsp_store()
.read(cx)
.applicable_inlay_chunks(buffer_id, ranges)
}
fn resolve_inlay_hint(
fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
self.read(cx).lsp_store().update(cx, |lsp_store, _| {
lsp_store.invalidate_inlay_hints(for_buffers)
});
}
fn inlay_hints(
&self,
hint: InlayHint,
buffer_handle: Entity<Buffer>,
server_id: LanguageServerId,
invalidate: InvalidationStrategy,
buffer: Entity<Buffer>,
ranges: Vec<Range<text::Anchor>>,
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
cx: &mut App,
) -> Option<Task<anyhow::Result<InlayHint>>> {
Some(self.update(cx, |project, cx| {
project.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
) -> Option<HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>>> {
Some(self.read(cx).lsp_store().update(cx, |lsp_store, cx| {
lsp_store.inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
}))
}
@@ -23460,16 +23185,6 @@ impl SemanticsProvider for Entity<Project> {
}
}
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
cx: &mut Context<Editor>,
) -> InlayHintSettings {
let file = snapshot.file_at(location);
let language = snapshot.language_at(location).map(|l| l.name());
language_settings(language, file, cx).inlay_hints
}
fn consume_contiguous_rows(
contiguous_row_selections: &mut Vec<Selection<Point>>,
selection: &Selection<Point>,
+3 -1
View File
@@ -31,6 +31,7 @@ use language::{
tree_sitter_python,
};
use language_settings::Formatter;
use languages::rust_lang;
use lsp::CompletionParams;
use multi_buffer::{IndentGuide, PathKey};
use parking_lot::Mutex;
@@ -50,7 +51,7 @@ use std::{
iter,
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use test::build_editor_with_project;
use text::ToPoint as _;
use unindent::Unindent;
use util::{
@@ -12640,6 +12641,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
);
}
});
cx.run_until_parked();
// Handle formatting requests to the language server.
cx.lsp
+6 -188
View File
@@ -1,19 +1,14 @@
use crate::{
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId,
Navigated, PointForPosition, SelectPhase,
editor_settings::GoToDefinitionFallback,
hover_popover::{self, InlayHover},
GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind,
Navigated, PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback,
scroll::ScrollAmount,
};
use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px};
use language::{Bias, ToOffset};
use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
ResolveState, ResolvedPath,
};
use project::{InlayId, LocationLink, Project, ResolvedPath};
use settings::Settings;
use std::ops::Range;
use theme::ActiveTheme as _;
@@ -138,10 +133,9 @@ impl Editor {
show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
}
None => {
update_inlay_link_and_hover_points(
self.update_inlay_link_and_hover_points(
snapshot,
point_for_position,
self,
hovered_link_modifier,
modifiers.shift,
window,
@@ -283,182 +277,6 @@ impl Editor {
}
}
pub fn update_inlay_link_and_hover_points(
snapshot: &EditorSnapshot,
point_for_position: PointForPosition,
editor: &mut Editor,
secondary_held: bool,
shift_held: bool,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
} else {
None
};
let mut go_to_definition_updated = false;
let mut hover_updated = false;
if let Some(hovered_offset) = hovered_offset {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let previous_valid_anchor =
buffer_snapshot.anchor_before(point_for_position.previous_valid.to_point(snapshot));
let next_valid_anchor =
buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.skip_while(|hint| {
hint.position
.cmp(&previous_valid_anchor, &buffer_snapshot)
.is_lt()
})
.take_while(|hint| {
hint.position
.cmp(&next_valid_anchor, &buffer_snapshot)
.is_le()
})
.max_by_key(|hint| hint.id)
{
let inlay_hint_cache = editor.inlay_hint_cache();
let excerpt_id = previous_valid_anchor.excerpt_id;
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
match cached_hint.resolve_state {
ResolveState::CanResolve(_, _) => {
if let Some(buffer_id) = snapshot
.buffer_snapshot()
.buffer_id_for_anchor(previous_valid_anchor)
{
inlay_hint_cache.spawn_hint_resolve(
buffer_id,
excerpt_id,
hovered_hint.id,
window,
cx,
);
}
}
ResolveState::Resolved => {
let mut extra_shift_left = 0;
let mut extra_shift_right = 0;
if cached_hint.padding_left {
extra_shift_left += 1;
extra_shift_right += 1;
}
if cached_hint.padding_right {
extra_shift_right += 1;
}
match cached_hint.label {
project::InlayHintLabel::String(_) => {
if let Some(tooltip) = cached_hint.tooltip {
hover_popover::hover_at_inlay(
editor,
InlayHover {
tooltip: match tooltip {
InlayHintTooltip::String(text) => HoverBlock {
text,
kind: HoverBlockKind::PlainText,
},
InlayHintTooltip::MarkupContent(content) => {
HoverBlock {
text: content.value,
kind: content.kind,
}
}
},
range: InlayHighlight {
inlay: hovered_hint.id,
inlay_position: hovered_hint.position,
range: extra_shift_left
..hovered_hint.text().len() + extra_shift_right,
},
},
window,
cx,
);
hover_updated = true;
}
}
project::InlayHintLabel::LabelParts(label_parts) => {
let hint_start =
snapshot.anchor_to_inlay_offset(hovered_hint.position);
if let Some((hovered_hint_part, part_range)) =
hover_popover::find_hovered_hint_part(
label_parts,
hint_start,
hovered_offset,
)
{
let highlight_start =
(part_range.start - hint_start).0 + extra_shift_left;
let highlight_end =
(part_range.end - hint_start).0 + extra_shift_right;
let highlight = InlayHighlight {
inlay: hovered_hint.id,
inlay_position: hovered_hint.position,
range: highlight_start..highlight_end,
};
if let Some(tooltip) = hovered_hint_part.tooltip {
hover_popover::hover_at_inlay(
editor,
InlayHover {
tooltip: match tooltip {
InlayHintLabelPartTooltip::String(text) => {
HoverBlock {
text,
kind: HoverBlockKind::PlainText,
}
}
InlayHintLabelPartTooltip::MarkupContent(
content,
) => HoverBlock {
text: content.value,
kind: content.kind,
},
},
range: highlight.clone(),
},
window,
cx,
);
hover_updated = true;
}
if let Some((language_server_id, location)) =
hovered_hint_part.location
&& secondary_held
&& !editor.has_pending_nonempty_selection()
{
go_to_definition_updated = true;
show_link_definition(
shift_held,
editor,
TriggerPoint::InlayHint(
highlight,
location,
language_server_id,
),
snapshot,
window,
cx,
);
}
}
}
};
}
ResolveState::Resolving => {}
}
}
}
}
if !go_to_definition_updated {
editor.hide_hovered_link(cx)
}
if !hover_updated {
hover_popover::hover_at(editor, None, window, cx);
}
}
pub fn show_link_definition(
shift_held: bool,
editor: &mut Editor,
@@ -912,7 +730,7 @@ mod tests {
DisplayPoint,
display_map::ToDisplayPoint,
editor_tests::init_test,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
test::editor_lsp_test_context::EditorLspTestContext,
};
use futures::StreamExt;
@@ -1343,7 +1161,7 @@ mod tests {
cx.background_executor.run_until_parked();
cx.update_editor(|editor, _window, cx| {
let expected_layers = vec![hint_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor));
assert_eq!(expected_layers, cached_hint_labels(editor, cx));
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
});
+7 -10
View File
@@ -986,17 +986,17 @@ impl DiagnosticPopover {
mod tests {
use super::*;
use crate::{
InlayId, PointForPosition,
PointForPosition,
actions::ConfirmCompletion,
editor_tests::{handle_completion_request, init_test},
hover_links::update_inlay_link_and_hover_points,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
test::editor_lsp_test_context::EditorLspTestContext,
};
use collections::BTreeSet;
use gpui::App;
use indoc::indoc;
use markdown::parser::MarkdownEvent;
use project::InlayId;
use settings::InlayHintSettingsContent;
use smol::stream::StreamExt;
use std::sync::atomic;
@@ -1648,7 +1648,7 @@ mod tests {
cx.background_executor.run_until_parked();
cx.update_editor(|editor, _, cx| {
let expected_layers = vec![entire_hint_label.to_string()];
assert_eq!(expected_layers, cached_hint_labels(editor));
assert_eq!(expected_layers, cached_hint_labels(editor, cx));
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
});
@@ -1687,10 +1687,9 @@ mod tests {
}
});
cx.update_editor(|editor, window, cx| {
update_inlay_link_and_hover_points(
editor.update_inlay_link_and_hover_points(
&editor.snapshot(window, cx),
new_type_hint_part_hover_position,
editor,
true,
false,
window,
@@ -1758,10 +1757,9 @@ mod tests {
cx.background_executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
update_inlay_link_and_hover_points(
editor.update_inlay_link_and_hover_points(
&editor.snapshot(window, cx),
new_type_hint_part_hover_position,
editor,
true,
false,
window,
@@ -1813,10 +1811,9 @@ mod tests {
}
});
cx.update_editor(|editor, window, cx| {
update_inlay_link_and_hover_points(
editor.update_inlay_link_and_hover_points(
&editor.snapshot(window, cx),
struct_hint_part_hover_position,
editor,
true,
false,
window,
+193
View File
@@ -0,0 +1,193 @@
//! The logic, responsible for managing [`Inlay`]s in the editor.
//!
//! Inlays are "not real" text that gets mixed into the "real" buffer's text.
//! They are attached to a certain [`Anchor`], and display certain contents (usually, strings)
//! between real text around that anchor.
//!
//! Inlay examples in Zed:
//! * inlay hints, received from LSP
//! * inline values, shown in the debugger
//! * inline predictions, showing the Zeta/Copilot/etc. predictions
//! * document color values, if configured to be displayed as inlays
//! * ... anything else, potentially.
//!
//! Editor uses [`crate::DisplayMap`] and [`crate::display_map::InlayMap`] to manage what's rendered inside the editor, using
//! [`InlaySplice`] to update this state.
/// Logic, related to managing LSP inlay hint inlays.
pub mod inlay_hints;
use std::{any::TypeId, sync::OnceLock};
use gpui::{Context, HighlightStyle, Hsla, Rgba, Task};
use multi_buffer::Anchor;
use project::{InlayHint, InlayId};
use text::Rope;
use crate::{Editor, hover_links::InlayHighlight};
/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
#[derive(Debug, Default)]
pub struct InlaySplice {
pub to_remove: Vec<InlayId>,
pub to_insert: Vec<Inlay>,
}
impl InlaySplice {
pub fn is_empty(&self) -> bool {
self.to_remove.is_empty() && self.to_insert.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct Inlay {
pub id: InlayId,
pub position: Anchor,
pub content: InlayContent,
}
#[derive(Debug, Clone)]
pub enum InlayContent {
Text(text::Rope),
Color(Hsla),
}
impl Inlay {
pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self {
let mut text = hint.text();
if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
text.push(" ");
}
if hint.padding_left && text.chars_at(0).next() != Some(' ') {
text.push_front(" ");
}
Self {
id,
position,
content: InlayContent::Text(text),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
Self {
id: InlayId::Hint(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn color(id: usize, position: Anchor, color: Rgba) -> Self {
Self {
id: InlayId::Color(id),
position,
content: InlayContent::Color(color.into()),
}
}
pub fn edit_prediction<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::EditPrediction(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::DebuggerValue(id),
position,
content: InlayContent::Text(text.into()),
}
}
pub fn text(&self) -> &Rope {
static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
match &self.content {
InlayContent::Text(text) => text,
InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("")),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn get_color(&self) -> Option<Hsla> {
match self.content {
InlayContent::Color(color) => Some(color),
_ => None,
}
}
}
pub struct InlineValueCache {
pub enabled: bool,
pub inlays: Vec<InlayId>,
pub refresh_task: Task<Option<()>>,
}
impl InlineValueCache {
pub fn new(enabled: bool) -> Self {
Self {
enabled,
inlays: Vec::new(),
refresh_task: Task::ready(None),
}
}
}
impl Editor {
/// Modify which hints are displayed in the editor.
pub fn splice_inlays(
&mut self,
to_remove: &[InlayId],
to_insert: Vec<Inlay>,
cx: &mut Context<Self>,
) {
if let Some(inlay_hints) = &mut self.inlay_hints {
for id_to_remove in to_remove {
inlay_hints.added_hints.remove(id_to_remove);
}
}
self.display_map.update(cx, |display_map, cx| {
display_map.splice_inlays(to_remove, to_insert, cx)
});
cx.notify();
}
pub(crate) fn highlight_inlays<T: 'static>(
&mut self,
highlights: Vec<InlayHighlight>,
style: HighlightStyle,
cx: &mut Context<Self>,
) {
self.display_map.update(cx, |map, _| {
map.highlight_inlays(TypeId::of::<T>(), highlights, style)
});
cx.notify();
}
pub fn inline_values_enabled(&self) -> bool {
self.inline_value_cache.enabled
}
#[cfg(any(test, feature = "test-support"))]
pub fn inline_value_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
.cloned()
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn all_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
self.display_map
.read(cx)
.current_inlays()
.cloned()
.collect()
}
}
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -6,15 +6,15 @@ use gpui::{Hsla, Rgba, Task};
use itertools::Itertools;
use language::point_from_lsp;
use multi_buffer::Anchor;
use project::DocumentColor;
use project::{DocumentColor, InlayId};
use settings::Settings as _;
use text::{Bias, BufferId, OffsetRangeExt as _};
use ui::{App, Context, Window};
use util::post_inc;
use crate::{
DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId,
InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode,
DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT,
InlaySplice, RangeToAnchorExt, editor_settings::DocumentColorsRenderMode, inlays::Inlay,
};
#[derive(Debug)]
@@ -164,7 +164,7 @@ impl Editor {
}
let visible_buffers = self
.visible_excerpts(None, cx)
.visible_excerpts(cx)
.into_values()
.map(|(buffer, ..)| buffer)
.filter(|editor_buffer| {
@@ -400,8 +400,7 @@ impl Editor {
}
if colors.render_mode == DocumentColorsRenderMode::Inlay
&& (!colors_splice.to_insert.is_empty()
|| !colors_splice.to_remove.is_empty())
&& !colors_splice.is_empty()
{
editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx);
updated = true;
+1 -1
View File
@@ -872,7 +872,7 @@ mod tests {
use super::*;
use crate::{
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
display_map::Inlay,
inlays::Inlay,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
};
use gpui::{AppContext as _, font, px};
+28 -19
View File
@@ -1,14 +1,14 @@
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
use buffer_diff::BufferDiff;
use collections::HashSet;
use collections::{HashMap, HashSet};
use futures::{channel::mpsc, future::join_all};
use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
use language::{Buffer, BufferEvent, Capability};
use language::{Buffer, BufferEvent, BufferRow, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project;
use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints};
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
use text::{BufferId, ToOffset};
use ui::{ButtonLike, KeyBinding, prelude::*};
use workspace::{
Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -436,14 +436,34 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
self.0.hover(&buffer, position, cx)
}
fn applicable_inlay_chunks(
&self,
buffer_id: BufferId,
ranges: &[Range<text::Anchor>],
cx: &App,
) -> Vec<Range<BufferRow>> {
self.0.applicable_inlay_chunks(buffer_id, ranges, cx)
}
fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
self.0.invalidate_inlay_hints(for_buffers, cx);
}
fn inlay_hints(
&self,
invalidate: InvalidationStrategy,
buffer: Entity<Buffer>,
range: Range<text::Anchor>,
ranges: Vec<Range<text::Anchor>>,
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
self.0.inlay_hints(buffer, range, cx)
) -> Option<HashMap<Range<BufferRow>, Task<anyhow::Result<CacheInlayHints>>>> {
let positions = ranges
.iter()
.flat_map(|range| [range.start, range.end])
.collect::<Vec<_>>();
let buffer = self.to_base(&buffer, &positions, cx)?;
self.0
.inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
}
fn inline_values(
@@ -455,17 +475,6 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
None
}
fn resolve_inlay_hint(
&self,
hint: project::InlayHint,
buffer: Entity<Buffer>,
server_id: lsp::LanguageServerId,
cx: &mut App,
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
let buffer = self.to_base(&buffer, &[], cx)?;
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
}
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
if let Some(buffer) = self.to_base(buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
@@ -6,6 +6,7 @@ use std::{
};
use anyhow::Result;
use language::rust_lang;
use serde_json::json;
use crate::{Editor, ToPoint};
@@ -32,55 +33,6 @@ pub struct EditorLspTestContext {
pub buffer_lsp_url: lsp::Uri,
}
pub(crate) fn rust_lang() -> Arc<Language> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
text_objects: Some(Cow::from(indoc! {r#"
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
"#})),
..Default::default()
})
.expect("Could not parse queries");
Arc::new(language)
}
#[cfg(test)]
pub(crate) fn git_commit_lang() -> Arc<Language> {
Arc::new(Language::new(
+59
View File
@@ -2600,6 +2600,65 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
start..end
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub fn rust_lang() -> Arc<Language> {
use std::borrow::Cow;
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(
r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#,
)),
brackets: Some(Cow::from(
r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#,
)),
text_objects: Some(Cow::from(
r#"
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
"#,
)),
..LanguageQueries::default()
})
.expect("Could not parse queries");
Arc::new(language)
}
#[cfg(test)]
mod tests {
use super::*;
+1 -1
View File
@@ -234,7 +234,7 @@ pub(crate) struct OnTypeFormatting {
pub push_to_history: bool,
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub(crate) struct InlayHints {
pub range: Range<Anchor>,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,221 @@
use std::{collections::hash_map, ops::Range, sync::Arc};
use collections::HashMap;
use futures::future::Shared;
use gpui::{App, Entity, Task};
use language::{Buffer, BufferRow, BufferSnapshot};
use lsp::LanguageServerId;
use text::OffsetRangeExt;
use crate::{InlayHint, InlayId};
pub type CacheInlayHints = HashMap<LanguageServerId, Vec<(InlayId, InlayHint)>>;
pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
#[derive(Debug, Clone, Copy)]
pub enum InvalidationStrategy {
/// Language servers reset hints via <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">request</a>.
/// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
///
/// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
RefreshRequested(LanguageServerId),
/// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
/// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
BufferEdited,
/// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
/// No invalidation should be done at all, all new hints are added to the cache.
///
/// A special case is the editor toggles and settings change:
/// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints.
/// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
None,
}
impl InvalidationStrategy {
pub fn should_invalidate(&self) -> bool {
matches!(
self,
InvalidationStrategy::RefreshRequested(_) | InvalidationStrategy::BufferEdited
)
}
}
pub struct BufferInlayHints {
snapshot: BufferSnapshot,
buffer_chunks: Vec<BufferChunk>,
hints_by_chunks: Vec<Option<CacheInlayHints>>,
fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
hints_by_id: HashMap<InlayId, HintForId>,
pub(super) hint_resolves: HashMap<InlayId, Shared<Task<()>>>,
}
#[derive(Debug, Clone, Copy)]
struct HintForId {
chunk_id: usize,
server_id: LanguageServerId,
position: usize,
}
/// An range of rows, exclusive as [`lsp::Range`] and
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
/// denote.
///
/// Represents an area in a text editor, adjacent to other ones.
/// Together, chunks form entire document at a particular version [`clock::Global`].
/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BufferChunk {
id: usize,
pub start: BufferRow,
pub end: BufferRow,
}
impl std::fmt::Debug for BufferInlayHints {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferInlayHints")
.field("buffer_chunks", &self.buffer_chunks)
.field("hints_by_chunks", &self.hints_by_chunks)
.field("fetches_by_chunks", &self.fetches_by_chunks)
.field("hints_by_id", &self.hints_by_id)
.finish_non_exhaustive()
}
}
const MAX_ROWS_IN_A_CHUNK: u32 = 50;
impl BufferInlayHints {
pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
let last_row = buffer_point_range.end.row;
let buffer_chunks = (buffer_point_range.start.row..=last_row)
.step_by(MAX_ROWS_IN_A_CHUNK as usize)
.enumerate()
.map(|(id, chunk_start)| BufferChunk {
id,
start: chunk_start,
end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
})
.collect::<Vec<_>>();
Self {
hints_by_chunks: vec![None; buffer_chunks.len()],
fetches_by_chunks: vec![None; buffer_chunks.len()],
hints_by_id: HashMap::default(),
hint_resolves: HashMap::default(),
snapshot,
buffer_chunks,
}
}
pub fn applicable_chunks(
&self,
ranges: &[Range<text::Anchor>],
) -> impl Iterator<Item = BufferChunk> {
let row_ranges = ranges
.iter()
.map(|range| range.to_point(&self.snapshot))
.map(|point_range| point_range.start.row..=point_range.end.row)
.collect::<Vec<_>>();
self.buffer_chunks
.iter()
.filter(move |chunk| -> bool {
// Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
// This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
let chunk_range = chunk.start..=chunk.end;
row_ranges.iter().any(|row_range| {
chunk_range.contains(&row_range.start())
|| chunk_range.contains(&row_range.end())
})
})
.copied()
}
pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
self.hints_by_chunks[chunk.id].as_ref()
}
pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
&mut self.fetches_by_chunks[chunk.id]
}
#[cfg(any(test, feature = "test-support"))]
pub fn all_cached_hints(&self) -> Vec<InlayHint> {
self.hints_by_chunks
.iter()
.filter_map(|hints| hints.as_ref())
.flat_map(|hints| hints.values().cloned())
.flatten()
.map(|(_, hint)| hint)
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn all_fetched_hints(&self) -> Vec<CacheInlayHintsTask> {
self.fetches_by_chunks
.iter()
.filter_map(|fetches| fetches.clone())
.collect()
}
pub fn remove_server_data(&mut self, for_server: LanguageServerId) {
for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() {
if let Some(hints) = hints {
if hints.remove(&for_server).is_some() {
self.fetches_by_chunks[chunk_index] = None;
}
}
}
}
pub fn clear(&mut self) {
self.hints_by_chunks = vec![None; self.buffer_chunks.len()];
self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
self.hints_by_id.clear();
self.hint_resolves.clear();
}
pub fn insert_new_hints(
&mut self,
chunk: BufferChunk,
server_id: LanguageServerId,
new_hints: Vec<(InlayId, InlayHint)>,
) {
let existing_hints = self.hints_by_chunks[chunk.id]
.get_or_insert_default()
.entry(server_id)
.or_insert_with(Vec::new);
let existing_count = existing_hints.len();
existing_hints.extend(new_hints.into_iter().enumerate().filter_map(
|(i, (id, new_hint))| {
let new_hint_for_id = HintForId {
chunk_id: chunk.id,
server_id,
position: existing_count + i,
};
if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) {
vacant_entry.insert(new_hint_for_id);
Some((id, new_hint))
} else {
None
}
},
));
*self.fetched_hints(&chunk) = None;
}
pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> {
let hint_for_id = self.hints_by_id.get(&id)?;
let (hint_id, hint) = self
.hints_by_chunks
.get_mut(hint_for_id.chunk_id)?
.as_mut()?
.get_mut(&hint_for_id.server_id)?
.get_mut(hint_for_id.position)?;
debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}");
Some(hint)
}
}
+28 -30
View File
@@ -145,9 +145,9 @@ pub use task_inventory::{
pub use buffer_store::ProjectTransaction;
pub use lsp_store::{
DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest,
LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
SERVER_PROGRESS_THROTTLE_TIMEOUT,
DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress,
LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore,
LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
pub use toolchain_store::{ToolchainStore, Toolchains};
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@@ -338,7 +338,7 @@ pub enum Event {
HostReshared,
Reshared,
Rejoined,
RefreshInlayHints,
RefreshInlayHints(LanguageServerId),
RefreshCodeLens,
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
@@ -402,6 +402,26 @@ pub enum PrepareRenameResponse {
InvalidPosition,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
EditPrediction(usize),
DebuggerValue(usize),
// LSP
Hint(usize),
Color(usize),
}
impl InlayId {
pub fn id(&self) -> usize {
match self {
Self::EditPrediction(id) => *id,
Self::DebuggerValue(id) => *id,
Self::Hint(id) => *id,
Self::Color(id) => *id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHint {
pub position: language::Anchor,
@@ -3058,7 +3078,9 @@ impl Project {
return;
};
}
LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
LspStoreEvent::RefreshInlayHints(server_id) => {
cx.emit(Event::RefreshInlayHints(*server_id))
}
LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))
@@ -3978,31 +4000,6 @@ impl Project {
})
}
pub fn inlay_hints<T: ToOffset>(
&mut self,
buffer_handle: Entity<Buffer>,
range: Range<T>,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.inlay_hints(buffer_handle, range, cx)
})
}
pub fn resolve_inlay_hint(
&self,
hint: InlayHint,
buffer_handle: Entity<Buffer>,
server_id: LanguageServerId,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<InlayHint>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
})
}
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
let (result_tx, result_rx) = smol::channel::unbounded();
@@ -5262,6 +5259,7 @@ impl Project {
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool {
self.lsp_store.update(cx, |this, cx| {
this.language_servers_for_local_buffer(buffer, cx)
+8 -2
View File
@@ -1815,7 +1815,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
fake_server
.start_progress(format!("{}/0", progress_token))
.await;
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
assert_eq!(
events.next().await.unwrap(),
Event::RefreshInlayHints(fake_server.server.server_id())
);
assert_eq!(
events.next().await.unwrap(),
Event::DiskBasedDiagnosticsStarted {
@@ -1954,7 +1957,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
Some(worktree_id)
)
);
assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
assert_eq!(
events.next().await.unwrap(),
Event::RefreshInlayHints(fake_server.server.server_id())
);
fake_server.start_progress(progress_token).await;
assert_eq!(
events.next().await.unwrap(),
+4
View File
@@ -465,6 +465,7 @@ message ResolveInlayHintResponse {
message RefreshInlayHints {
uint64 project_id = 1;
uint64 server_id = 2;
}
message CodeLens {
@@ -781,6 +782,7 @@ message TextEdit {
message LspQuery {
uint64 project_id = 1;
uint64 lsp_request_id = 2;
optional uint64 server_id = 15;
oneof request {
GetReferences get_references = 3;
GetDocumentColor get_document_color = 4;
@@ -793,6 +795,7 @@ message LspQuery {
GetDeclaration get_declaration = 11;
GetTypeDefinition get_type_definition = 12;
GetImplementation get_implementation = 13;
InlayHints inlay_hints = 14;
}
}
@@ -815,6 +818,7 @@ message LspResponse {
GetTypeDefinitionResponse get_type_definition_response = 10;
GetImplementationResponse get_implementation_response = 11;
GetReferencesResponse get_references_response = 12;
InlayHintsResponse inlay_hints_response = 13;
}
uint64 server_id = 7;
}
+2
View File
@@ -517,6 +517,7 @@ lsp_messages!(
(GetDeclaration, GetDeclarationResponse, true),
(GetTypeDefinition, GetTypeDefinitionResponse, true),
(GetImplementation, GetImplementationResponse, true),
(InlayHints, InlayHintsResponse, false),
);
entity_messages!(
@@ -847,6 +848,7 @@ impl LspQuery {
Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false),
Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false),
Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false),
Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false),
None => ("<unknown>", true),
}
}
+5
View File
@@ -226,6 +226,7 @@ impl AnyProtoClient {
pub fn request_lsp<T>(
&self,
project_id: u64,
server_id: Option<u64>,
timeout: Duration,
executor: BackgroundExecutor,
request: T,
@@ -247,6 +248,7 @@ impl AnyProtoClient {
let query = proto::LspQuery {
project_id,
server_id,
lsp_request_id: new_id.0,
request: Some(request.to_proto_query()),
};
@@ -361,6 +363,9 @@ impl AnyProtoClient {
Response::GetImplementationResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::InlayHintsResponse(response) => {
to_any_envelope(&envelope, response)
}
};
Some(proto::ProtoLspResponse {
server_id,
+2
View File
@@ -47,5 +47,7 @@ zed_actions.workspace = true
client = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
+97 -1
View File
@@ -2357,9 +2357,10 @@ pub mod tests {
use super::*;
use editor::{DisplayPoint, display_map::DisplayRow};
use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
use language::{FakeLspAdapter, rust_lang};
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use settings::{InlayHintSettingsContent, SettingsStore};
use util::{path, paths::PathStyle, rel_path::rel_path};
use util_macros::perf;
use workspace::DeploySearch;
@@ -4226,6 +4227,101 @@ pub mod tests {
.unwrap();
}
#[perf]
#[gpui::test]
async fn test_search_with_inlays(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.inlay_hints =
Some(InlayHintSettingsContent {
enabled: Some(true),
..InlayHintSettingsContent::default()
})
});
});
});
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/dir"),
// `\n` , a trailing line on the end, is important for the test case
json!({
"main.rs": "fn main() { let a = 2; }\n",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let language = rust_lang();
language_registry.add(language);
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(|fake_server| {
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
move |_, _| async move {
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, 17),
label: lsp::InlayHintLabel::String(": i32".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
},
);
})),
..FakeLspAdapter::default()
},
);
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
let search_view = cx.add_window(|window, cx| {
ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
});
perform_search(search_view, "let ", cx);
let _fake_server = fake_servers.next().await.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
search_view
.update(cx, |search_view, _, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nfn main() { let a: i32 = 2; }\n"
);
})
.unwrap();
// Can do the 2nd search without any panics
perform_search(search_view, "let ", cx);
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
search_view
.update(cx, |search_view, _, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nfn main() { let a: i32 = 2; }\n"
);
})
.unwrap();
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
+1 -1
View File
@@ -934,7 +934,7 @@ where
/// 2. When encountering digits, treating consecutive digits as a single number
/// 3. Comparing numbers by their numeric value rather than lexicographically
/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
fn natural_sort(a: &str, b: &str) -> Ordering {
pub fn natural_sort(a: &str, b: &str) -> Ordering {
let mut a_iter = a.chars().peekable();
let mut b_iter = b.chars().peekable();
+1 -1
View File
@@ -3083,7 +3083,7 @@ mod test {
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
use editor::display_map::Inlay;
use editor::Inlay;
use indoc::indoc;
use language::Point;
use multi_buffer::MultiBufferRow;