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