Further improve color inlay hints in multi buffers (#33642)

Follow-up of https://github.com/zed-industries/zed/pull/33605

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-06-30 12:18:43 +03:00 committed by GitHub
parent ac3328adb6
commit ae6237178c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 230 additions and 128 deletions

View file

@ -37,7 +37,9 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
pub use fold_map::{
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@ -538,7 +540,7 @@ impl DisplayMap {
pub fn update_fold_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);

View file

@ -1,4 +1,4 @@
use crate::display_map::inlay_map::InlayChunk;
use crate::{InlayId, display_map::inlay_map::InlayChunk};
use super::{
Highlights,
@ -277,13 +277,16 @@ impl FoldMapWriter<'_> {
pub(crate) fn update_fold_widths(
&mut self,
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
let ChunkRendererId::Fold(id) = id else {
continue;
};
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
@ -529,7 +532,7 @@ impl FoldMap {
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
id: fold.id,
id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@ -1267,11 +1270,17 @@ pub struct Chunk<'a> {
pub renderer: Option<ChunkRenderer>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkRendererId {
Fold(FoldId),
Inlay(InlayId),
}
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
/// The id of the fold associated with this chunk.
pub id: FoldId,
/// The id of the renderer associated with this chunk.
pub id: ChunkRendererId,
/// Creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.

View file

@ -1,4 +1,4 @@
use crate::{ChunkRenderer, HighlightStyles, InlayId, display_map::FoldId};
use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
@ -14,7 +14,7 @@ use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
use super::{Highlights, custom_highlights::CustomHighlightsChunks};
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
/// Decides where the [`Inlay`]s should be displayed.
///
@ -338,10 +338,10 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
InlayId::Color(id) => {
InlayId::Color(_) => {
if let Some(color) = inlay.color {
renderer = Some(ChunkRenderer {
id: FoldId(id),
id: ChunkRendererId::Inlay(inlay.id),
render: Arc::new(move |cx| {
div()
.w_4()

View file

@ -17333,9 +17333,9 @@ impl Editor {
self.active_indent_guides_state.dirty = true;
}
pub fn update_fold_widths(
pub fn update_renderer_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
self.display_map

View file

@ -12,8 +12,8 @@ use crate::{
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
HighlightedChunk, ToDisplayPoint,
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
HighlightKey, HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
@ -7119,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
enum LineFragment {
Text(ShapedLine),
Element {
id: FoldId,
id: ChunkRendererId,
element: Option<AnyElement>,
size: Size<Pixels>,
len: usize,
@ -8297,7 +8297,7 @@ impl Element for EditorElement {
window,
cx,
);
let new_fold_widths = line_layouts
let new_renrerer_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
@ -8308,7 +8308,7 @@ impl Element for EditorElement {
}
});
if self.editor.update(cx, |editor, cx| {
editor.update_fold_widths(new_fold_widths, cx)
editor.update_renderer_widths(new_renrerer_widths, cx)
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in

View file

@ -19,18 +19,21 @@ use crate::{
#[derive(Debug)]
pub(super) struct LspColorData {
cache_version_used: usize,
buffer_colors: HashMap<BufferId, BufferColors>,
render_mode: DocumentColorsRenderMode,
}
#[derive(Debug, Default)]
struct BufferColors {
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
inlay_colors: HashMap<InlayId, usize>,
render_mode: DocumentColorsRenderMode,
cache_version_used: usize,
}
impl LspColorData {
pub fn new(cx: &App) -> Self {
Self {
cache_version_used: 0,
colors: Vec::new(),
inlay_colors: HashMap::default(),
buffer_colors: HashMap::default(),
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
}
}
@ -47,8 +50,9 @@ impl LspColorData {
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
to_remove: Vec::new(),
to_insert: self
.colors
.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
.map(|(range, color, id)| {
Inlay::color(
id.id(),
@ -63,33 +67,49 @@ impl LspColorData {
})
.collect(),
}),
DocumentColorsRenderMode::None => {
self.colors.clear();
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_insert: Vec::new(),
})
}
DocumentColorsRenderMode::None => Some(InlaySplice {
to_remove: self
.buffer_colors
.drain()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
}),
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_remove: self
.buffer_colors
.iter_mut()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
})
}
}
}
fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
if self.colors == colors {
fn set_colors(
&mut self,
buffer_id: BufferId,
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
cache_version: Option<usize>,
) -> bool {
let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
if let Some(cache_version) = cache_version {
buffer_colors.cache_version_used = cache_version;
}
if buffer_colors.colors == colors {
return false;
}
self.inlay_colors = colors
buffer_colors.inlay_colors = colors
.iter()
.enumerate()
.map(|(i, (_, _, id))| (*id, i))
.collect();
self.colors = colors;
buffer_colors.colors = colors;
true
}
@ -103,8 +123,9 @@ impl LspColorData {
{
Vec::new()
} else {
self.colors
self.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| &buffer_colors.colors)
.map(|(range, color, _)| {
let display_range = range.clone().to_display_points(snapshot);
let color = Hsla::from(Rgba {
@ -162,10 +183,9 @@ impl Editor {
ColorFetchStrategy::IgnoreCache
} else {
ColorFetchStrategy::UseCache {
known_cache_version: self
.colors
.as_ref()
.map(|colors| colors.cache_version_used),
known_cache_version: self.colors.as_ref().and_then(|colors| {
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
}),
}
};
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
@ -201,15 +221,13 @@ impl Editor {
return;
};
let mut cache_version = None;
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
let mut new_editor_colors = HashMap::default();
for (buffer_id, colors) in all_colors {
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
continue;
};
match colors {
Ok(colors) => {
cache_version = colors.cache_version;
for color in colors.colors {
let color_start = point_from_lsp(color.lsp_range.start);
let color_end = point_from_lsp(color.lsp_range.end);
@ -243,8 +261,15 @@ impl Editor {
continue;
};
let new_entry =
new_editor_colors.entry(buffer_id).or_insert_with(|| {
(Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
});
new_entry.1 = colors.cache_version;
let new_buffer_colors = &mut new_entry.0;
let (Ok(i) | Err(i)) =
new_editor_colors.binary_search_by(|(probe, _)| {
new_buffer_colors.binary_search_by(|(probe, _)| {
probe
.start
.cmp(&color_start_anchor, &multi_buffer_snapshot)
@ -254,7 +279,7 @@ impl Editor {
.cmp(&color_end_anchor, &multi_buffer_snapshot)
})
});
new_editor_colors
new_buffer_colors
.insert(i, (color_start_anchor..color_end_anchor, color));
break;
}
@ -267,45 +292,70 @@ impl Editor {
editor
.update(cx, |editor, cx| {
let mut colors_splice = InlaySplice::default();
let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
let Some(colors) = &mut editor.colors else {
return;
};
let mut existing_colors = colors.colors.iter().peekable();
for (new_range, new_color) in new_editor_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
let mut updated = false;
for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
let mut new_buffer_color_inlays =
Vec::with_capacity(new_buffer_colors.len());
let mut existing_buffer_colors = colors
.buffer_colors
.entry(buffer_id)
.or_default()
.colors
.iter()
.peekable();
for (new_range, new_color) in new_buffer_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
loop {
match existing_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
loop {
match existing_buffer_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_buffer_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_buffer_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
colors_splice
.to_remove
.push(*existing_inlay_id);
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
}
existing_buffer_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
@ -313,49 +363,40 @@ impl Editor {
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
existing_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
}
}
}
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
}
}
}
}
if existing_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_colors.map(|(_, _, id)| *id));
if existing_buffer_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_buffer_colors.map(|(_, _, id)| *id));
}
updated |= colors.set_colors(
buffer_id,
new_buffer_color_inlays,
new_cache_version,
);
}
let mut updated = colors.set_colors(new_color_inlays);
if let Some(cache_version) = cache_version {
colors.cache_version_used = cache_version;
}
if colors.render_mode == DocumentColorsRenderMode::Inlay
&& (!colors_splice.to_insert.is_empty()
|| !colors_splice.to_remove.is_empty())

View file

@ -170,6 +170,7 @@ pub struct LocalLspStore {
_subscription: gpui::Subscription,
lsp_tree: Entity<LanguageServerTree>,
registered_buffers: HashMap<BufferId, usize>,
buffers_opened_in_servers: HashMap<BufferId, HashSet<LanguageServerId>>,
buffer_pull_diagnostics_result_ids: HashMap<LanguageServerId, HashMap<PathBuf, Option<String>>>,
}
@ -2546,6 +2547,10 @@ impl LocalLspStore {
vec![snapshot]
});
self.buffers_opened_in_servers
.entry(buffer_id)
.or_default()
.insert(server.server_id());
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id: server.server_id(),
name: None,
@ -3208,6 +3213,9 @@ impl LocalLspStore {
self.language_servers.remove(server_id_to_remove);
self.buffer_pull_diagnostics_result_ids
.remove(server_id_to_remove);
for buffer_servers in self.buffers_opened_in_servers.values_mut() {
buffer_servers.remove(server_id_to_remove);
}
cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove));
}
servers_to_remove.into_keys().collect()
@ -3787,6 +3795,7 @@ impl LspStore {
}),
lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx),
registered_buffers: HashMap::default(),
buffers_opened_in_servers: HashMap::default(),
buffer_pull_diagnostics_result_ids: HashMap::default(),
}),
last_formatting_failure: None,
@ -4159,6 +4168,7 @@ impl LspStore {
lsp_store.lsp_data.remove(&buffer_id);
let local = lsp_store.as_local_mut().unwrap();
local.registered_buffers.remove(&buffer_id);
local.buffers_opened_in_servers.remove(&buffer_id);
if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() {
local.unregister_old_buffer_from_language_servers(&buffer, &file, cx);
}
@ -6235,21 +6245,31 @@ impl LspStore {
} => {
if let Some(cached_data) = self.lsp_data.get(&buffer_id) {
if !version_queried_for.changed_since(&cached_data.colors_for_version) {
if Some(cached_data.cache_version) == known_cache_version {
return None;
} else {
return Some(
Task::ready(Ok(DocumentColors {
colors: cached_data
.colors
.values()
.flatten()
.cloned()
.collect(),
cache_version: Some(cached_data.cache_version),
}))
.shared(),
);
let has_different_servers = self.as_local().is_some_and(|local| {
local
.buffers_opened_in_servers
.get(&buffer_id)
.cloned()
.unwrap_or_default()
!= cached_data.colors.keys().copied().collect()
});
if !has_different_servers {
if Some(cached_data.cache_version) == known_cache_version {
return None;
} else {
return Some(
Task::ready(Ok(DocumentColors {
colors: cached_data
.colors
.values()
.flatten()
.cloned()
.collect(),
cache_version: Some(cached_data.cache_version),
}))
.shared(),
);
}
}
}
}
@ -7522,6 +7542,14 @@ impl LspStore {
.unwrap_or(true)
})
.map(|(_, server)| server.server_id())
.filter(|server_id| {
self.as_local().is_none_or(|local| {
local
.buffers_opened_in_servers
.get(&snapshot.remote_id())
.is_some_and(|servers| servers.contains(server_id))
})
})
.collect::<Vec<_>>()
});
@ -10084,6 +10112,7 @@ impl LspStore {
}
// Tell the language server about every open buffer in the worktree that matches the language.
let mut buffer_paths_registered = Vec::new();
self.buffer_store.clone().update(cx, |buffer_store, cx| {
for buffer_handle in buffer_store.buffers() {
let buffer = buffer_handle.read(cx);
@ -10142,6 +10171,12 @@ impl LspStore {
version,
initial_snapshot.text(),
);
buffer_paths_registered.push(file.abs_path(cx));
local
.buffers_opened_in_servers
.entry(buffer.remote_id())
.or_default()
.insert(server_id);
}
buffer_handle.update(cx, |buffer, cx| {
buffer.set_completion_triggers(
@ -10163,6 +10198,18 @@ impl LspStore {
}
});
for abs_path in buffer_paths_registered {
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id: server_id,
name: Some(adapter.name()),
message: proto::update_language_server::Variant::RegisteredForBuffer(
proto::RegisteredForBuffer {
buffer_abs_path: abs_path.to_string_lossy().to_string(),
},
),
});
}
cx.notify();
}
@ -10612,6 +10659,9 @@ impl LspStore {
}
if let Some(local) = self.as_local_mut() {
local.buffer_pull_diagnostics_result_ids.remove(&for_server);
for buffer_servers in local.buffers_opened_in_servers.values_mut() {
buffer_servers.remove(&for_server);
}
}
}