Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:03:14.534Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

highlights_tree_view.rs

1338 lines · 48.1 KB · rust
1use editor::{
2    Anchor, Editor, HighlightKey, MultiBufferSnapshot, SelectionEffects, ToPoint,
3    scroll::Autoscroll,
4};
5use gpui::{
6    Action, App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
7    Focusable, HighlightStyle, Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
8    MouseMoveEvent, ParentElement, Render, ScrollStrategy, SharedString, Styled, Task,
9    UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list,
10};
11use language::{BufferId, Point, ToOffset};
12use menu::{SelectNext, SelectPrevious};
13use std::{mem, ops::Range, sync::Arc, time::Duration};
14use theme::ActiveTheme;
15use theme::SyntaxTheme;
16use ui::{
17    ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, WithScrollbar, prelude::*,
18};
19
20use workspace::{
21    Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
22    ToolbarItemView, Workspace,
23    item::{Item, ItemHandle},
24};
25
26actions!(
27    dev,
28    [
29        /// Opens the highlights tree view for the current file.
30        OpenHighlightsTreeView,
31    ]
32);
33
34actions!(
35    highlights_tree_view,
36    [
37        /// Toggles showing text highlights.
38        ToggleTextHighlights,
39        /// Toggles showing semantic token highlights.
40        ToggleSemanticTokens,
41        /// Toggles showing syntax token highlights.
42        ToggleSyntaxTokens,
43    ]
44);
45
46pub fn init(cx: &mut App) {
47    cx.observe_new(move |workspace: &mut Workspace, _, _| {
48        workspace.register_action(move |workspace, _: &OpenHighlightsTreeView, window, cx| {
49            let active_item = workspace.active_item(cx);
50            let workspace_handle = workspace.weak_handle();
51            let highlights_tree_view =
52                cx.new(|cx| HighlightsTreeView::new(workspace_handle, active_item, window, cx));
53            workspace.split_item(
54                SplitDirection::Right,
55                Box::new(highlights_tree_view),
56                window,
57                cx,
58            )
59        });
60    })
61    .detach();
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
65pub enum HighlightCategory {
66    Text(HighlightKey),
67    SyntaxToken {
68        capture_name: SharedString,
69        theme_key: Option<SharedString>,
70    },
71    SemanticToken {
72        token_type: Option<SharedString>,
73        token_modifiers: Option<SharedString>,
74        theme_key: Option<SharedString>,
75    },
76}
77
78impl HighlightCategory {
79    fn label(&self) -> SharedString {
80        match self {
81            HighlightCategory::Text(key) => format!("text: {key:?}").into(),
82            HighlightCategory::SyntaxToken {
83                capture_name,
84                theme_key: Some(theme_key),
85            } => format!("syntax: {capture_name} \u{2192} {theme_key}").into(),
86            HighlightCategory::SyntaxToken {
87                capture_name,
88                theme_key: None,
89            } => format!("syntax: {capture_name}").into(),
90            HighlightCategory::SemanticToken {
91                token_type,
92                token_modifiers,
93                theme_key,
94            } => {
95                let label = match (token_type, token_modifiers) {
96                    (Some(token_type), Some(modifiers)) => {
97                        format!("semantic token: {token_type} [{modifiers}]")
98                    }
99                    (Some(token_type), None) => format!("semantic token: {token_type}"),
100                    (None, Some(modifiers)) => format!("semantic token [{modifiers}]"),
101                    (None, None) => "semantic token".to_string(),
102                };
103
104                if let Some(theme_key) = theme_key {
105                    format!("{label} \u{2192} {theme_key}").into()
106                } else {
107                    label.into()
108                }
109            }
110        }
111    }
112}
113
114#[derive(Debug, Clone)]
115struct HighlightEntry {
116    range: Range<Anchor>,
117    buffer_id: BufferId,
118    buffer_point_range: Range<Point>,
119    range_display: SharedString,
120    style: HighlightStyle,
121    category: HighlightCategory,
122}
123
124/// An item in the display list: either a separator between excerpts or a highlight entry.
125#[derive(Debug, Clone)]
126enum DisplayItem {
127    ExcerptSeparator {
128        label: SharedString,
129    },
130    Entry {
131        /// Index into `cached_entries`.
132        entry_ix: usize,
133    },
134}
135
136pub struct HighlightsTreeView {
137    workspace_handle: WeakEntity<Workspace>,
138    editor: Option<EditorState>,
139    list_scroll_handle: UniformListScrollHandle,
140    selected_item_ix: Option<usize>,
141    hovered_item_ix: Option<usize>,
142    focus_handle: FocusHandle,
143    cached_entries: Vec<HighlightEntry>,
144    display_items: Vec<DisplayItem>,
145    is_singleton: bool,
146    show_text_highlights: bool,
147    show_syntax_tokens: bool,
148    show_semantic_tokens: bool,
149    skip_next_scroll: bool,
150    refresh_task: Task<()>,
151}
152
153pub struct HighlightsTreeToolbarItemView {
154    tree_view: Option<Entity<HighlightsTreeView>>,
155    _subscription: Option<gpui::Subscription>,
156    toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
157}
158
159struct EditorState {
160    editor: Entity<Editor>,
161    _subscription: gpui::Subscription,
162}
163
164struct SemanticHighlightEntry {
165    range: Range<Anchor>,
166    style: HighlightStyle,
167    category: HighlightCategory,
168}
169
170struct HighlightRefreshInput {
171    multi_buffer_snapshot: MultiBufferSnapshot,
172    text_highlights: Vec<(HighlightKey, Arc<(HighlightStyle, Vec<Range<Anchor>>)>)>,
173    semantic_highlights: Vec<SemanticHighlightEntry>,
174    syntax_theme: Arc<SyntaxTheme>,
175}
176
177impl HighlightsTreeView {
178    pub fn new(
179        workspace_handle: WeakEntity<Workspace>,
180        active_item: Option<Box<dyn ItemHandle>>,
181        window: &mut Window,
182        cx: &mut Context<Self>,
183    ) -> Self {
184        let mut this = Self {
185            workspace_handle: workspace_handle.clone(),
186            list_scroll_handle: UniformListScrollHandle::new(),
187            editor: None,
188            hovered_item_ix: None,
189            selected_item_ix: None,
190            focus_handle: cx.focus_handle(),
191            cached_entries: Vec::new(),
192            display_items: Vec::new(),
193            is_singleton: true,
194            show_text_highlights: true,
195            show_syntax_tokens: true,
196            show_semantic_tokens: true,
197            skip_next_scroll: false,
198            refresh_task: Task::ready(()),
199        };
200
201        this.handle_item_updated(active_item, window, cx);
202
203        cx.subscribe_in(
204            &workspace_handle.upgrade().unwrap(),
205            window,
206            move |this, workspace, event, window, cx| match event {
207                WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
208                    this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
209                }
210                WorkspaceEvent::ItemRemoved { item_id } => {
211                    this.handle_item_removed(item_id, window, cx);
212                }
213                _ => {}
214            },
215        )
216        .detach();
217
218        this
219    }
220
221    fn handle_item_updated(
222        &mut self,
223        active_item: Option<Box<dyn ItemHandle>>,
224        window: &mut Window,
225        cx: &mut Context<Self>,
226    ) {
227        let active_editor = match active_item {
228            Some(active_item) => {
229                if active_item.item_id() == cx.entity_id() {
230                    return;
231                } else {
232                    match active_item.downcast::<Editor>() {
233                        Some(active_editor) => active_editor,
234                        None => {
235                            self.clear(cx);
236                            return;
237                        }
238                    }
239                }
240            }
241            None => {
242                self.clear(cx);
243                return;
244            }
245        };
246
247        let is_different_editor = self
248            .editor
249            .as_ref()
250            .is_none_or(|state| state.editor != active_editor);
251        if is_different_editor {
252            self.set_editor(active_editor, window, cx);
253        }
254    }
255
256    fn handle_item_removed(
257        &mut self,
258        item_id: &EntityId,
259        _window: &mut Window,
260        cx: &mut Context<Self>,
261    ) {
262        if self
263            .editor
264            .as_ref()
265            .is_some_and(|state| state.editor.entity_id() == *item_id)
266        {
267            self.clear(cx);
268        }
269    }
270
271    fn clear(&mut self, cx: &mut Context<Self>) {
272        self.refresh_task = Task::ready(());
273        self.cached_entries.clear();
274        self.display_items.clear();
275        self.selected_item_ix = None;
276        self.hovered_item_ix = None;
277        if let Some(state) = self.editor.take() {
278            Self::clear_editor_highlights(&state.editor, cx);
279        }
280        cx.notify();
281    }
282
283    fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
284        if let Some(state) = &self.editor {
285            if state.editor == editor {
286                return;
287            }
288            Self::clear_editor_highlights(&state.editor, cx);
289        }
290
291        let subscription =
292            cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
293                editor::EditorEvent::Reparsed(_) => this.schedule_refresh_highlights(window, cx),
294                editor::EditorEvent::SelectionsChanged { .. } => {
295                    this.update_selection_from_editor(cx);
296                }
297                _ => return,
298            });
299
300        self.editor = Some(EditorState {
301            editor,
302            _subscription: subscription,
303        });
304        self.refresh_task = Task::ready(());
305        self.cached_entries.clear();
306        self.display_items.clear();
307        self.selected_item_ix = None;
308        self.hovered_item_ix = None;
309        self.schedule_refresh_highlights(window, cx);
310        cx.notify();
311    }
312
313    fn schedule_refresh_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
314        self.refresh_task = cx.spawn_in(window, async move |this, cx| {
315            cx.background_executor()
316                .timer(Duration::from_millis(30))
317                .await;
318
319            let Some(input) = this
320                .update(cx, |this, cx| this.highlight_refresh_input(cx))
321                .ok()
322                .flatten()
323            else {
324                return;
325            };
326
327            let new_highlights = cx
328                .background_spawn(async move { build_highlight_entries(input) })
329                .await;
330
331            this.update_in(cx, |this, _window, cx| {
332                this.apply_highlight_refresh(new_highlights, cx);
333            })
334            .ok();
335        });
336    }
337
338    fn highlight_refresh_input(&self, cx: &mut Context<Self>) -> Option<HighlightRefreshInput> {
339        let editor_state = self.editor.as_ref()?;
340        let editor = editor_state.editor.read(cx);
341        let display_map = editor.display_map.clone();
342        let project = editor.project().cloned()?;
343        let multi_buffer = editor.buffer().clone();
344        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
345        let syntax_theme = cx.theme().syntax().clone();
346
347        let (text_highlights, semantic_token_highlights) =
348            display_map.update(cx, |display_map, _| {
349                let text_highlights = display_map
350                    .all_text_highlights()
351                    .map(|(key, highlights)| (*key, highlights.clone()))
352                    .collect::<Vec<_>>();
353                let semantic_token_highlights = display_map
354                    .all_semantic_token_highlights()
355                    .map(|(buffer_id, (tokens, interner))| {
356                        (*buffer_id, tokens.clone(), interner.clone())
357                    })
358                    .collect::<Vec<_>>();
359                (text_highlights, semantic_token_highlights)
360            });
361
362        let mut semantic_highlights = Vec::new();
363        project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
364            for (buffer_id, tokens, interner) in semantic_token_highlights {
365                let language_name = multi_buffer
366                    .read(cx)
367                    .buffer(buffer_id)
368                    .and_then(|buffer| buffer.read(cx).language().map(|language| language.name()));
369
370                for token in tokens.iter() {
371                    let Some(stylizer) = lsp_store.get_or_create_token_stylizer(
372                        token.server_id,
373                        language_name.as_ref(),
374                        cx,
375                    ) else {
376                        continue;
377                    };
378
379                    let theme_key = stylizer
380                        .rules_for_token(token.token_type)
381                        .and_then(|rules| {
382                            rules
383                                .iter()
384                                .filter(|rule| {
385                                    rule.token_modifiers.iter().all(|modifier| {
386                                        stylizer.has_modifier(token.token_modifiers, modifier)
387                                    })
388                                })
389                                .fold(None, |theme_key, rule| {
390                                    rule.style
391                                        .iter()
392                                        .find(|style_name| {
393                                            syntax_theme.style_for_name(style_name).is_some()
394                                        })
395                                        .map(|style_name| SharedString::from(style_name.clone()))
396                                        .or(theme_key)
397                                })
398                        });
399
400                    semantic_highlights.push(SemanticHighlightEntry {
401                        range: token.range.start..token.range.end,
402                        style: interner[token.style],
403                        category: HighlightCategory::SemanticToken {
404                            token_type: stylizer.token_type_name(token.token_type).cloned(),
405                            token_modifiers: stylizer
406                                .token_modifiers(token.token_modifiers)
407                                .map(SharedString::from),
408                            theme_key,
409                        },
410                    });
411                }
412            }
413        });
414
415        Some(HighlightRefreshInput {
416            multi_buffer_snapshot,
417            text_highlights,
418            semantic_highlights,
419            syntax_theme,
420        })
421    }
422
423    fn apply_highlight_refresh(
424        &mut self,
425        new_highlights: Vec<HighlightEntry>,
426        cx: &mut Context<Self>,
427    ) {
428        let Some(editor) = self.editor.as_ref().map(|state| state.editor.clone()) else {
429            return;
430        };
431        let editor = editor.read(cx);
432        let cursor_position = editor.selections.newest_anchor().head();
433        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
434
435        self.is_singleton = multi_buffer_snapshot.is_singleton();
436        self.cached_entries = new_highlights;
437        self.hovered_item_ix = None;
438        self.rebuild_display_items(&multi_buffer_snapshot, cx);
439
440        let should_scroll = !mem::take(&mut self.skip_next_scroll);
441        self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, should_scroll);
442        self.sync_editor_highlight_for_selected_entry(cx);
443        cx.notify();
444    }
445
446    fn rebuild_display_items(&mut self, snapshot: &MultiBufferSnapshot, cx: &App) {
447        self.display_items.clear();
448
449        let mut last_range_end = None;
450        for (entry_ix, entry) in self.cached_entries.iter().enumerate() {
451            if !self.should_show_entry(entry) {
452                continue;
453            }
454
455            if !self.is_singleton {
456                let excerpt_changed = last_range_end.is_none_or(|anchor| {
457                    snapshot
458                        .excerpt_containing(anchor..entry.range.start)
459                        .is_none()
460                });
461                if excerpt_changed {
462                    last_range_end = Some(entry.range.end);
463                    let label = excerpt_label_for(entry, snapshot, cx);
464                    self.display_items
465                        .push(DisplayItem::ExcerptSeparator { label });
466                }
467            }
468
469            self.display_items.push(DisplayItem::Entry { entry_ix });
470        }
471    }
472
473    fn should_show_entry(&self, entry: &HighlightEntry) -> bool {
474        match entry.category {
475            HighlightCategory::Text(_) => self.show_text_highlights,
476            HighlightCategory::SyntaxToken { .. } => self.show_syntax_tokens,
477            HighlightCategory::SemanticToken { .. } => self.show_semantic_tokens,
478        }
479    }
480
481    fn update_selection_from_editor(&mut self, cx: &mut Context<Self>) {
482        let Some(editor_state) = self.editor.as_ref() else {
483            return;
484        };
485
486        let (cursor_position, multi_buffer_snapshot) = {
487            let editor = editor_state.editor.read(cx);
488            (
489                editor.selections.newest_anchor().head(),
490                editor.buffer().read(cx).snapshot(cx),
491            )
492        };
493
494        let should_update_selection = !mem::take(&mut self.skip_next_scroll);
495        if !should_update_selection {
496            self.sync_editor_highlight_for_selected_entry(cx);
497            return;
498        }
499
500        let should_notify =
501            self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, true);
502        self.sync_editor_highlight_for_selected_entry(cx);
503        if should_notify {
504            cx.notify();
505        }
506    }
507
508    fn update_selection_for_cursor(
509        &mut self,
510        cursor: &Anchor,
511        snapshot: &MultiBufferSnapshot,
512        should_scroll: bool,
513    ) -> bool {
514        let cursor_point = cursor.to_point(snapshot);
515        let Some((cursor_buffer, cursor_point)) = snapshot.point_to_buffer_point(cursor_point)
516        else {
517            let changed = self.selected_item_ix.take().is_some();
518            return changed;
519        };
520        let cursor_buffer_id = cursor_buffer.remote_id();
521
522        let best = self
523            .display_items
524            .iter()
525            .enumerate()
526            .filter_map(|(display_ix, item)| match item {
527                DisplayItem::Entry { entry_ix } => {
528                    let entry = &self.cached_entries[*entry_ix];
529                    Some((display_ix, *entry_ix, entry))
530                }
531                _ => None,
532            })
533            .filter(|(_, _, entry)| {
534                entry.buffer_id == cursor_buffer_id
535                    && entry.buffer_point_range.start <= cursor_point
536                    && cursor_point < entry.buffer_point_range.end
537            })
538            .min_by_key(|(_, _, entry)| {
539                (
540                    entry
541                        .buffer_point_range
542                        .end
543                        .row
544                        .saturating_sub(entry.buffer_point_range.start.row),
545                    entry
546                        .buffer_point_range
547                        .end
548                        .column
549                        .saturating_sub(entry.buffer_point_range.start.column),
550                )
551            })
552            .map(|(display_ix, entry_ix, _)| (display_ix, entry_ix));
553
554        let selected_item_ix = best.map(|(_, entry_ix)| entry_ix);
555        let changed = self.selected_item_ix != selected_item_ix;
556        self.selected_item_ix = selected_item_ix;
557
558        if should_scroll && let Some((display_ix, _)) = best {
559            self.list_scroll_handle
560                .scroll_to_item(display_ix, ScrollStrategy::Center);
561            return true;
562        }
563
564        changed
565    }
566
567    fn update_editor_with_range_for_entry(
568        &self,
569        entry_ix: usize,
570        window: &mut Window,
571        cx: &mut Context<Self>,
572        f: &mut dyn FnMut(&mut Editor, Range<Anchor>, usize, &mut Window, &mut Context<Editor>),
573    ) -> Option<()> {
574        let editor_state = self.editor.as_ref()?;
575        let entry = self.cached_entries.get(entry_ix)?;
576        let range = entry.range.clone();
577        let key = cx.entity_id().as_u64() as usize;
578
579        editor_state.editor.update(cx, |editor, cx| {
580            f(editor, range, key, window, cx);
581        });
582        Some(())
583    }
584
585    fn render_entry(&self, entry: &HighlightEntry, selected: bool, cx: &App) -> Div {
586        let colors = cx.theme().colors();
587        let style_preview = render_style_preview(entry.style, selected, cx);
588
589        h_flex()
590            .gap_1()
591            .child(style_preview)
592            .child(Label::new(entry.range_display.clone()).color(Color::Default))
593            .child(
594                Label::new(entry.category.label())
595                    .size(LabelSize::Small)
596                    .color(Color::Muted),
597            )
598            .text_bg(if selected {
599                colors.element_selected
600            } else {
601                Hsla::default()
602            })
603            .pl(rems(0.5))
604            .hover(|style| style.bg(colors.element_hover))
605    }
606
607    fn render_separator(&self, label: &SharedString, cx: &App) -> Div {
608        let colors = cx.theme().colors();
609        h_flex()
610            .gap_1()
611            .px(rems(0.5))
612            .bg(colors.surface_background)
613            .border_b_1()
614            .border_color(colors.border_variant)
615            .child(
616                Label::new(label.clone())
617                    .size(LabelSize::Small)
618                    .color(Color::Muted),
619            )
620    }
621
622    fn compute_items(
623        &mut self,
624        visible_range: Range<usize>,
625        _window: &mut Window,
626        cx: &mut Context<Self>,
627    ) -> Vec<Div> {
628        let mut items = Vec::new();
629
630        for display_ix in visible_range {
631            let Some(display_item) = self.display_items.get(display_ix) else {
632                continue;
633            };
634
635            match display_item {
636                DisplayItem::ExcerptSeparator { label } => {
637                    items.push(self.render_separator(label, cx));
638                }
639                DisplayItem::Entry { entry_ix } => {
640                    let entry_ix = *entry_ix;
641                    let entry = &self.cached_entries[entry_ix];
642                    let selected = Some(entry_ix) == self.selected_item_ix;
643                    let rendered = self
644                        .render_entry(entry, selected, cx)
645                        .on_mouse_down(
646                            MouseButton::Left,
647                            cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
648                                tree_view.selected_item_ix = Some(entry_ix);
649                                tree_view.skip_next_scroll = true;
650                                tree_view.update_editor_with_range_for_entry(
651                                    entry_ix,
652                                    window,
653                                    cx,
654                                    &mut |editor, mut range, _, window, cx| {
655                                        mem::swap(&mut range.start, &mut range.end);
656                                        editor.change_selections(
657                                            SelectionEffects::scroll(Autoscroll::newest()),
658                                            window,
659                                            cx,
660                                            |selections| {
661                                                selections.select_ranges([range]);
662                                            },
663                                        );
664                                    },
665                                );
666                                cx.notify();
667                            }),
668                        )
669                        .on_mouse_move(cx.listener(
670                            move |tree_view, _: &MouseMoveEvent, window, cx| {
671                                if tree_view.hovered_item_ix != Some(entry_ix) {
672                                    tree_view.hovered_item_ix = Some(entry_ix);
673                                    tree_view.update_editor_with_range_for_entry(
674                                        entry_ix,
675                                        window,
676                                        cx,
677                                        &mut |editor, range, key, _, cx| {
678                                            Self::set_editor_highlights(editor, key, &[range], cx);
679                                        },
680                                    );
681                                    cx.notify();
682                                }
683                            },
684                        ));
685
686                    items.push(rendered);
687                }
688            }
689        }
690
691        items
692    }
693
694    fn set_editor_highlights(
695        editor: &mut Editor,
696        key: usize,
697        ranges: &[Range<Anchor>],
698        cx: &mut Context<Editor>,
699    ) {
700        editor.highlight_background(
701            HighlightKey::HighlightsTreeView(key),
702            ranges,
703            |_, theme| theme.colors().editor_document_highlight_write_background,
704            cx,
705        );
706    }
707
708    fn sync_editor_highlight_for_selected_entry(&self, cx: &mut Context<Self>) {
709        let Some(editor_state) = self.editor.as_ref() else {
710            return;
711        };
712        let key = cx.entity_id().as_u64() as usize;
713        let range = self
714            .selected_item_ix
715            .and_then(|entry_ix| self.cached_entries.get(entry_ix))
716            .map(|entry| entry.range.clone());
717
718        editor_state.editor.update(cx, |editor, cx| {
719            if let Some(range) = range {
720                Self::set_editor_highlights(editor, key, &[range], cx);
721            } else {
722                editor.clear_background_highlights(HighlightKey::HighlightsTreeView(key), cx);
723            }
724        });
725    }
726
727    fn clear_editor_highlights(editor: &Entity<Editor>, cx: &mut Context<Self>) {
728        let highlight_key = HighlightKey::HighlightsTreeView(cx.entity_id().as_u64() as usize);
729        editor.update(cx, |editor, cx| {
730            editor.clear_background_highlights(highlight_key, cx);
731        });
732    }
733
734    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
735        self.move_selection(-1, window, cx);
736    }
737
738    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
739        self.move_selection(1, window, cx);
740    }
741
742    fn move_selection(&mut self, delta: i32, window: &mut Window, cx: &mut Context<Self>) {
743        if self.display_items.is_empty() {
744            return;
745        }
746
747        let entry_display_items: Vec<(usize, usize)> = self
748            .display_items
749            .iter()
750            .enumerate()
751            .filter_map(|(display_ix, item)| match item {
752                DisplayItem::Entry { entry_ix } => Some((display_ix, *entry_ix)),
753                _ => None,
754            })
755            .collect();
756
757        if entry_display_items.is_empty() {
758            return;
759        }
760
761        let current_pos = self
762            .selected_item_ix
763            .and_then(|selected| {
764                entry_display_items
765                    .iter()
766                    .position(|(_, entry_ix)| *entry_ix == selected)
767            })
768            .unwrap_or(0);
769
770        let new_pos = if delta < 0 {
771            current_pos.saturating_sub((-delta) as usize)
772        } else {
773            (current_pos + delta as usize).min(entry_display_items.len() - 1)
774        };
775
776        if let Some(&(display_ix, entry_ix)) = entry_display_items.get(new_pos) {
777            self.selected_item_ix = Some(entry_ix);
778            self.skip_next_scroll = true;
779            self.list_scroll_handle
780                .scroll_to_item(display_ix, ScrollStrategy::Center);
781
782            self.update_editor_with_range_for_entry(
783                entry_ix,
784                window,
785                cx,
786                &mut |editor, mut range, _, window, cx| {
787                    mem::swap(&mut range.start, &mut range.end);
788                    editor.change_selections(
789                        SelectionEffects::scroll(Autoscroll::newest()),
790                        window,
791                        cx,
792                        |selections| {
793                            selections.select_ranges([range]);
794                        },
795                    );
796                },
797            );
798
799            cx.notify();
800        }
801    }
802
803    fn entry_count(&self) -> usize {
804        self.cached_entries
805            .iter()
806            .filter(|entry| self.should_show_entry(entry))
807            .count()
808    }
809}
810
811impl Render for HighlightsTreeView {
812    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
813        let display_count = self.display_items.len();
814
815        div()
816            .flex_1()
817            .track_focus(&self.focus_handle)
818            .key_context("HighlightsTreeView")
819            .on_action(cx.listener(Self::select_previous))
820            .on_action(cx.listener(Self::select_next))
821            .bg(cx.theme().colors().editor_background)
822            .map(|this| {
823                if display_count > 0 {
824                    this.child(
825                        uniform_list(
826                            "HighlightsTreeView",
827                            display_count,
828                            cx.processor(move |this, range: Range<usize>, window, cx| {
829                                this.compute_items(range, window, cx)
830                            }),
831                        )
832                        .size_full()
833                        .track_scroll(&self.list_scroll_handle)
834                        .text_bg(cx.theme().colors().background)
835                        .into_any_element(),
836                    )
837                    .vertical_scrollbar_for(&self.list_scroll_handle, window, cx)
838                    .into_any_element()
839                } else {
840                    let inner_content = v_flex()
841                        .items_center()
842                        .text_center()
843                        .gap_2()
844                        .max_w_3_5()
845                        .map(|this| {
846                            if self.editor.is_some() {
847                                let has_any = !self.cached_entries.is_empty();
848                                if has_any {
849                                    this.child(Label::new("All highlights are filtered out"))
850                                        .child(
851                                            Label::new(
852                                                "Enable text, syntax, or semantic highlights in the toolbar",
853                                            )
854                                            .size(LabelSize::Small),
855                                        )
856                                } else {
857                                    this.child(Label::new("No highlights found")).child(
858                                        Label::new(
859                                            "The editor has no text, syntax, or semantic token highlights",
860                                        )
861                                        .size(LabelSize::Small),
862                                    )
863                                }
864                            } else {
865                                this.child(Label::new("Not attached to an editor")).child(
866                                    Label::new("Focus an editor to show highlights")
867                                        .size(LabelSize::Small),
868                                )
869                            }
870                        });
871
872                    this.h_flex()
873                        .size_full()
874                        .justify_center()
875                        .child(inner_content)
876                        .into_any_element()
877                }
878            })
879    }
880}
881
882impl EventEmitter<()> for HighlightsTreeView {}
883
884impl Focusable for HighlightsTreeView {
885    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
886        self.focus_handle.clone()
887    }
888}
889
890impl Item for HighlightsTreeView {
891    type Event = ();
892
893    fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}
894
895    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
896        "Highlights".into()
897    }
898
899    fn telemetry_event_text(&self) -> Option<&'static str> {
900        None
901    }
902
903    fn can_split(&self) -> bool {
904        true
905    }
906
907    fn clone_on_split(
908        &self,
909        _: Option<workspace::WorkspaceId>,
910        window: &mut Window,
911        cx: &mut Context<Self>,
912    ) -> Task<Option<Entity<Self>>>
913    where
914        Self: Sized,
915    {
916        Task::ready(Some(cx.new(|cx| {
917            let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
918            clone.show_text_highlights = self.show_text_highlights;
919            clone.show_syntax_tokens = self.show_syntax_tokens;
920            clone.show_semantic_tokens = self.show_semantic_tokens;
921            clone.skip_next_scroll = false;
922            if let Some(editor) = &self.editor {
923                clone.set_editor(editor.editor.clone(), window, cx)
924            }
925            clone
926        })))
927    }
928
929    fn on_removed(&self, cx: &mut Context<Self>) {
930        if let Some(state) = self.editor.as_ref() {
931            Self::clear_editor_highlights(&state.editor, cx);
932        }
933    }
934}
935
936impl Default for HighlightsTreeToolbarItemView {
937    fn default() -> Self {
938        Self::new()
939    }
940}
941
942impl HighlightsTreeToolbarItemView {
943    pub fn new() -> Self {
944        Self {
945            tree_view: None,
946            _subscription: None,
947            toggle_settings_handle: PopoverMenuHandle::default(),
948        }
949    }
950
951    fn render_header(&self, cx: &Context<Self>) -> Option<ButtonLike> {
952        let tree_view = self.tree_view.as_ref()?;
953        let tree_view = tree_view.read(cx);
954
955        let total = tree_view.cached_entries.len();
956        let filtered = tree_view.entry_count();
957
958        let label = if filtered == total {
959            format!("{} highlights", total)
960        } else {
961            format!("{} / {} highlights", filtered, total)
962        };
963
964        Some(ButtonLike::new("highlights header").child(Label::new(label)))
965    }
966
967    fn render_settings_button(&self, cx: &Context<Self>) -> PopoverMenu<ContextMenu> {
968        let (show_text, show_syntax, show_semantic) = self
969            .tree_view
970            .as_ref()
971            .map(|view| {
972                let v = view.read(cx);
973                (
974                    v.show_text_highlights,
975                    v.show_syntax_tokens,
976                    v.show_semantic_tokens,
977                )
978            })
979            .unwrap_or((true, true, true));
980
981        let tree_view = self.tree_view.as_ref().map(|v| v.downgrade());
982
983        PopoverMenu::new("highlights-tree-settings")
984            .trigger_with_tooltip(
985                IconButton::new("toggle-highlights-settings-icon", IconName::Filter)
986                    .icon_size(IconSize::Small)
987                    .toggle_state(self.toggle_settings_handle.is_deployed()),
988                Tooltip::text("Highlights Settings"),
989            )
990            .anchor(gpui::Anchor::TopRight)
991            .with_handle(self.toggle_settings_handle.clone())
992            .menu(move |window, cx| {
993                let tree_view_for_text = tree_view.clone();
994                let tree_view_for_syntax = tree_view.clone();
995                let tree_view_for_semantic = tree_view.clone();
996
997                let menu = ContextMenu::build(window, cx, move |menu, _, _| {
998                    menu.toggleable_entry(
999                        "Text Highlights",
1000                        show_text,
1001                        IconPosition::Start,
1002                        Some(ToggleTextHighlights.boxed_clone()),
1003                        {
1004                            let tree_view = tree_view_for_text.clone();
1005                            move |_, cx| {
1006                                if let Some(view) = tree_view.as_ref() {
1007                                    view.update(cx, |view, cx| {
1008                                        view.show_text_highlights = !view.show_text_highlights;
1009                                        let snapshot = view.editor.as_ref().map(|s| {
1010                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
1011                                        });
1012                                        if let Some(snapshot) = snapshot {
1013                                            view.rebuild_display_items(&snapshot, cx);
1014                                        }
1015                                        cx.notify();
1016                                    })
1017                                    .ok();
1018                                }
1019                            }
1020                        },
1021                    )
1022                    .toggleable_entry(
1023                        "Syntax Tokens",
1024                        show_syntax,
1025                        IconPosition::Start,
1026                        Some(ToggleSyntaxTokens.boxed_clone()),
1027                        {
1028                            let tree_view = tree_view_for_syntax.clone();
1029                            move |_, cx| {
1030                                if let Some(view) = tree_view.as_ref() {
1031                                    view.update(cx, |view, cx| {
1032                                        view.show_syntax_tokens = !view.show_syntax_tokens;
1033                                        let snapshot = view.editor.as_ref().map(|s| {
1034                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
1035                                        });
1036                                        if let Some(snapshot) = snapshot {
1037                                            view.rebuild_display_items(&snapshot, cx);
1038                                        }
1039                                        cx.notify();
1040                                    })
1041                                    .ok();
1042                                }
1043                            }
1044                        },
1045                    )
1046                    .toggleable_entry(
1047                        "Semantic Tokens",
1048                        show_semantic,
1049                        IconPosition::Start,
1050                        Some(ToggleSemanticTokens.boxed_clone()),
1051                        {
1052                            move |_, cx| {
1053                                if let Some(view) = tree_view_for_semantic.as_ref() {
1054                                    view.update(cx, |view, cx| {
1055                                        view.show_semantic_tokens = !view.show_semantic_tokens;
1056                                        let snapshot = view.editor.as_ref().map(|s| {
1057                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
1058                                        });
1059                                        if let Some(snapshot) = snapshot {
1060                                            view.rebuild_display_items(&snapshot, cx);
1061                                        }
1062                                        cx.notify();
1063                                    })
1064                                    .ok();
1065                                }
1066                            }
1067                        },
1068                    )
1069                });
1070
1071                Some(menu)
1072            })
1073    }
1074}
1075
1076impl Render for HighlightsTreeToolbarItemView {
1077    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1078        h_flex()
1079            .gap_1()
1080            .children(self.render_header(cx))
1081            .child(self.render_settings_button(cx))
1082    }
1083}
1084
1085impl EventEmitter<ToolbarItemEvent> for HighlightsTreeToolbarItemView {}
1086
1087impl ToolbarItemView for HighlightsTreeToolbarItemView {
1088    fn set_active_pane_item(
1089        &mut self,
1090        active_pane_item: Option<&dyn ItemHandle>,
1091        window: &mut Window,
1092        cx: &mut Context<Self>,
1093    ) -> ToolbarItemLocation {
1094        if let Some(item) = active_pane_item
1095            && let Some(view) = item.downcast::<HighlightsTreeView>()
1096        {
1097            self.tree_view = Some(view.clone());
1098            self._subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
1099            return ToolbarItemLocation::PrimaryLeft;
1100        }
1101        self.tree_view = None;
1102        self._subscription = None;
1103        ToolbarItemLocation::Hidden
1104    }
1105}
1106
1107fn excerpt_label_for(
1108    entry: &HighlightEntry,
1109    snapshot: &MultiBufferSnapshot,
1110    cx: &App,
1111) -> SharedString {
1112    let path_label = snapshot
1113        .anchor_to_buffer_anchor(entry.range.start)
1114        .and_then(|(anchor, _)| snapshot.buffer_for_id(anchor.buffer_id))
1115        .and_then(|buf| buf.file())
1116        .map(|file| {
1117            let full_path = file.full_path(cx);
1118            full_path.to_string_lossy().to_string()
1119        })
1120        .unwrap_or_else(|| "untitled".to_string());
1121    path_label.into()
1122}
1123
1124fn build_highlight_entries(
1125    HighlightRefreshInput {
1126        multi_buffer_snapshot,
1127        text_highlights,
1128        semantic_highlights,
1129        syntax_theme,
1130    }: HighlightRefreshInput,
1131) -> Vec<HighlightEntry> {
1132    let mut entries = Vec::new();
1133
1134    for (key, text_highlights) in text_highlights {
1135        for range in &text_highlights.1 {
1136            let Some((range_display, buffer_id, buffer_point_range)) =
1137                format_anchor_range(range, &multi_buffer_snapshot)
1138            else {
1139                continue;
1140            };
1141            entries.push(HighlightEntry {
1142                range: range.clone(),
1143                buffer_id,
1144                range_display,
1145                style: text_highlights.0,
1146                category: HighlightCategory::Text(key),
1147                buffer_point_range,
1148            });
1149        }
1150    }
1151
1152    for highlight in semantic_highlights {
1153        let Some((range_display, buffer_id, buffer_point_range)) =
1154            format_anchor_range(&highlight.range, &multi_buffer_snapshot)
1155        else {
1156            continue;
1157        };
1158
1159        entries.push(HighlightEntry {
1160            range: highlight.range,
1161            buffer_id,
1162            range_display,
1163            style: highlight.style,
1164            category: highlight.category,
1165            buffer_point_range,
1166        });
1167    }
1168
1169    for excerpt_range in multi_buffer_snapshot.excerpts() {
1170        let Some(buffer_snapshot) =
1171            multi_buffer_snapshot.buffer_for_id(excerpt_range.context.start.buffer_id)
1172        else {
1173            continue;
1174        };
1175
1176        let start_offset = excerpt_range.context.start.to_offset(buffer_snapshot);
1177        let end_offset = excerpt_range.context.end.to_offset(buffer_snapshot);
1178        let range = start_offset..end_offset;
1179
1180        let captures = buffer_snapshot.captures(range, |grammar| {
1181            grammar
1182                .highlights_config
1183                .as_ref()
1184                .map(|config| &config.query)
1185        });
1186        let grammars = captures.grammars().to_vec();
1187        let highlight_maps = grammars
1188            .iter()
1189            .map(|grammar| grammar.highlight_map())
1190            .collect::<Vec<_>>();
1191
1192        for capture in captures {
1193            let Some(highlight_id) = highlight_maps[capture.grammar_index].get(capture.index)
1194            else {
1195                continue;
1196            };
1197            let Some(style) = syntax_theme.get(highlight_id).cloned() else {
1198                continue;
1199            };
1200
1201            let theme_key = syntax_theme
1202                .get_capture_name(highlight_id)
1203                .map(|theme_key| SharedString::from(theme_key.to_string()));
1204
1205            let capture_name = grammars[capture.grammar_index]
1206                .highlights_config
1207                .as_ref()
1208                .and_then(|config| config.query.capture_names().get(capture.index as usize))
1209                .map(|capture_name| SharedString::from((*capture_name).to_string()))
1210                .unwrap_or_else(|| SharedString::from("unknown"));
1211
1212            let start_anchor = buffer_snapshot.anchor_before(capture.node.start_byte());
1213            let end_anchor = buffer_snapshot.anchor_after(capture.node.end_byte());
1214
1215            let start = multi_buffer_snapshot.anchor_in_excerpt(start_anchor);
1216            let end = multi_buffer_snapshot.anchor_in_excerpt(end_anchor);
1217
1218            let (start, end) = match (start, end) {
1219                (Some(start), Some(end)) => (start, end),
1220                _ => continue,
1221            };
1222
1223            let range = start..end;
1224            let Some((range_display, buffer_id, buffer_point_range)) =
1225                format_anchor_range(&range, &multi_buffer_snapshot)
1226            else {
1227                continue;
1228            };
1229
1230            entries.push(HighlightEntry {
1231                range,
1232                buffer_id,
1233                range_display,
1234                style,
1235                category: HighlightCategory::SyntaxToken {
1236                    capture_name,
1237                    theme_key,
1238                },
1239                buffer_point_range,
1240            });
1241        }
1242    }
1243
1244    entries.sort_by(|a, b| {
1245        a.buffer_id
1246            .cmp(&b.buffer_id)
1247            .then_with(|| a.buffer_point_range.start.cmp(&b.buffer_point_range.start))
1248            .then_with(|| a.buffer_point_range.end.cmp(&b.buffer_point_range.end))
1249            .then_with(|| a.category.cmp(&b.category))
1250    });
1251    entries.dedup_by(|a, b| {
1252        a.buffer_id == b.buffer_id
1253            && a.buffer_point_range == b.buffer_point_range
1254            && a.category == b.category
1255    });
1256
1257    entries
1258}
1259
1260fn format_anchor_range(
1261    range: &Range<Anchor>,
1262    snapshot: &MultiBufferSnapshot,
1263) -> Option<(SharedString, BufferId, Range<Point>)> {
1264    let start = range.start.to_point(snapshot);
1265    let end = range.end.to_point(snapshot);
1266    let ((start_buffer, start), (_, end)) = snapshot
1267        .point_to_buffer_point(start)
1268        .zip(snapshot.point_to_buffer_point(end))?;
1269    let display = SharedString::from(format!(
1270        "[{}:{} - {}:{}]",
1271        start.row + 1,
1272        start.column + 1,
1273        end.row + 1,
1274        end.column + 1,
1275    ));
1276    Some((display, start_buffer.remote_id(), start..end))
1277}
1278
1279fn render_style_preview(style: HighlightStyle, selected: bool, cx: &App) -> Div {
1280    let colors = cx.theme().colors();
1281
1282    let display_color = style.color.or(style.background_color);
1283
1284    let mut preview = div().px_1().rounded_sm();
1285
1286    if let Some(color) = display_color {
1287        if selected {
1288            preview = preview.border_1().border_color(color).text_color(color);
1289        } else {
1290            preview = preview.bg(color);
1291        }
1292    } else {
1293        preview = preview.bg(colors.element_background);
1294    }
1295
1296    let mut parts = Vec::new();
1297
1298    if let Some(color) = display_color {
1299        parts.push(format_hsla_as_hex(color));
1300    }
1301    if style.font_weight.is_some() {
1302        parts.push("bold".to_string());
1303    }
1304    if style.font_style.is_some() {
1305        parts.push("italic".to_string());
1306    }
1307    if style.strikethrough.is_some() {
1308        parts.push("strike".to_string());
1309    }
1310    if style.underline.is_some() {
1311        parts.push("underline".to_string());
1312    }
1313
1314    let label_text = if parts.is_empty() {
1315        "none".to_string()
1316    } else {
1317        parts.join(" ")
1318    };
1319
1320    preview.child(Label::new(label_text).size(LabelSize::Small).when_some(
1321        display_color.filter(|_| selected),
1322        |label, display_color| label.color(Color::Custom(display_color)),
1323    ))
1324}
1325
1326fn format_hsla_as_hex(color: Hsla) -> String {
1327    let rgba = color.to_rgb();
1328    let r = (rgba.r * 255.0).round() as u8;
1329    let g = (rgba.g * 255.0).round() as u8;
1330    let b = (rgba.b * 255.0).round() as u8;
1331    let a = (rgba.a * 255.0).round() as u8;
1332    if a == 255 {
1333        format!("#{:02X}{:02X}{:02X}", r, g, b)
1334    } else {
1335        format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
1336    }
1337}
1338
Served at tenant.openagents/omega Member data and write actions are omitted.