1use editor::{
2 Anchor, Editor, ExcerptId, HighlightKey, MultiBufferSnapshot, SelectionEffects, ToPoint,
3 scroll::Autoscroll,
4};
5use gpui::{
6 Action, App, AppContext as _, Context, Corner, Div, Entity, EntityId, EventEmitter,
7 FocusHandle, Focusable, HighlightStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
8 MouseDownEvent, MouseMoveEvent, ParentElement, Render, ScrollStrategy, SharedString, Styled,
9 Task, UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list,
10};
11use menu::{SelectNext, SelectPrevious};
12use std::{mem, ops::Range};
13use theme::ActiveTheme;
14use ui::{
15 ButtonCommon, ButtonLike, ButtonStyle, Color, ContextMenu, FluentBuilder as _, IconButton,
16 IconName, IconPosition, IconSize, Label, LabelCommon, LabelSize, PopoverMenu,
17 PopoverMenuHandle, StyledExt, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
18};
19use workspace::{
20 Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
21 ToolbarItemView, Workspace,
22 item::{Item, ItemHandle},
23};
24
25actions!(
26 dev,
27 [
28 /// Opens the highlights tree view for the current file.
29 OpenHighlightsTreeView,
30 ]
31);
32
33actions!(
34 highlights_tree_view,
35 [
36 /// Toggles showing text highlights.
37 ToggleTextHighlights,
38 /// Toggles showing semantic token highlights.
39 ToggleSemanticTokens,
40 ]
41);
42
43pub fn init(cx: &mut App) {
44 cx.observe_new(move |workspace: &mut Workspace, _, _| {
45 workspace.register_action(move |workspace, _: &OpenHighlightsTreeView, window, cx| {
46 let active_item = workspace.active_item(cx);
47 let workspace_handle = workspace.weak_handle();
48 let highlights_tree_view =
49 cx.new(|cx| HighlightsTreeView::new(workspace_handle, active_item, window, cx));
50 workspace.split_item(
51 SplitDirection::Right,
52 Box::new(highlights_tree_view),
53 window,
54 cx,
55 )
56 });
57 })
58 .detach();
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
62pub enum HighlightCategory {
63 Text(HighlightKey),
64 SemanticToken {
65 token_type: Option<SharedString>,
66 token_modifiers: Option<SharedString>,
67 },
68}
69
70impl HighlightCategory {
71 fn label(&self) -> SharedString {
72 match self {
73 HighlightCategory::Text(key) => format!("text: {key:?}").into(),
74 HighlightCategory::SemanticToken {
75 token_type: Some(token_type),
76 token_modifiers: Some(modifiers),
77 } => format!("semantic token: {token_type} [{modifiers}]").into(),
78 HighlightCategory::SemanticToken {
79 token_type: Some(token_type),
80 token_modifiers: None,
81 } => format!("semantic token: {token_type}").into(),
82 HighlightCategory::SemanticToken {
83 token_type: None,
84 token_modifiers: Some(modifiers),
85 } => format!("semantic token [{modifiers}]").into(),
86 HighlightCategory::SemanticToken {
87 token_type: None,
88 token_modifiers: None,
89 } => "semantic token".into(),
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
95struct HighlightEntry {
96 excerpt_id: ExcerptId,
97 range: Range<Anchor>,
98 range_display: SharedString,
99 style: HighlightStyle,
100 category: HighlightCategory,
101 sort_key: (ExcerptId, u32, u32, u32, u32),
102}
103
104/// An item in the display list: either a separator between excerpts or a highlight entry.
105#[derive(Debug, Clone)]
106enum DisplayItem {
107 ExcerptSeparator {
108 label: SharedString,
109 },
110 Entry {
111 /// Index into `cached_entries`.
112 entry_ix: usize,
113 },
114}
115
116pub struct HighlightsTreeView {
117 workspace_handle: WeakEntity<Workspace>,
118 editor: Option<EditorState>,
119 list_scroll_handle: UniformListScrollHandle,
120 selected_item_ix: Option<usize>,
121 hovered_item_ix: Option<usize>,
122 focus_handle: FocusHandle,
123 cached_entries: Vec<HighlightEntry>,
124 display_items: Vec<DisplayItem>,
125 is_singleton: bool,
126 show_text_highlights: bool,
127 show_semantic_tokens: bool,
128 skip_next_scroll: bool,
129}
130
131pub struct HighlightsTreeToolbarItemView {
132 tree_view: Option<Entity<HighlightsTreeView>>,
133 _subscription: Option<gpui::Subscription>,
134 toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
135}
136
137struct EditorState {
138 editor: Entity<Editor>,
139 _subscription: gpui::Subscription,
140}
141
142impl HighlightsTreeView {
143 pub fn new(
144 workspace_handle: WeakEntity<Workspace>,
145 active_item: Option<Box<dyn ItemHandle>>,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) -> Self {
149 let mut this = Self {
150 workspace_handle: workspace_handle.clone(),
151 list_scroll_handle: UniformListScrollHandle::new(),
152 editor: None,
153 hovered_item_ix: None,
154 selected_item_ix: None,
155 focus_handle: cx.focus_handle(),
156 cached_entries: Vec::new(),
157 display_items: Vec::new(),
158 is_singleton: true,
159 show_text_highlights: true,
160 show_semantic_tokens: true,
161 skip_next_scroll: false,
162 };
163
164 this.handle_item_updated(active_item, window, cx);
165
166 cx.subscribe_in(
167 &workspace_handle.upgrade().unwrap(),
168 window,
169 move |this, workspace, event, window, cx| match event {
170 WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
171 this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
172 }
173 WorkspaceEvent::ItemRemoved { item_id } => {
174 this.handle_item_removed(item_id, window, cx);
175 }
176 _ => {}
177 },
178 )
179 .detach();
180
181 this
182 }
183
184 fn handle_item_updated(
185 &mut self,
186 active_item: Option<Box<dyn ItemHandle>>,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 let Some(editor) = active_item
191 .filter(|item| item.item_id() != cx.entity_id())
192 .and_then(|item| item.downcast::<Editor>())
193 else {
194 self.clear(cx);
195 return;
196 };
197
198 let is_different_editor = self
199 .editor
200 .as_ref()
201 .is_none_or(|state| state.editor != editor);
202 if is_different_editor {
203 self.set_editor(editor, window, cx);
204 }
205 }
206
207 fn handle_item_removed(
208 &mut self,
209 item_id: &EntityId,
210 _window: &mut Window,
211 cx: &mut Context<Self>,
212 ) {
213 if self
214 .editor
215 .as_ref()
216 .is_some_and(|state| state.editor.entity_id() == *item_id)
217 {
218 self.clear(cx);
219 }
220 }
221
222 fn clear(&mut self, cx: &mut Context<Self>) {
223 self.cached_entries.clear();
224 self.display_items.clear();
225 self.selected_item_ix = None;
226 self.hovered_item_ix = None;
227 if let Some(state) = self.editor.take() {
228 Self::clear_editor_highlights(&state.editor, cx);
229 }
230 cx.notify();
231 }
232
233 fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
234 if let Some(state) = &self.editor {
235 if state.editor == editor {
236 return;
237 }
238 Self::clear_editor_highlights(&state.editor, cx);
239 }
240
241 let subscription =
242 cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
243 editor::EditorEvent::Reparsed(_)
244 | editor::EditorEvent::SelectionsChanged { .. } => {
245 this.refresh_highlights(window, cx);
246 }
247 _ => return,
248 });
249
250 self.editor = Some(EditorState {
251 editor,
252 _subscription: subscription,
253 });
254 self.refresh_highlights(window, cx);
255 }
256
257 fn refresh_highlights(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
258 let Some(editor_state) = self.editor.as_ref() else {
259 self.clear(cx);
260 return;
261 };
262
263 let (display_map, project, multi_buffer, cursor_position) = {
264 let editor = editor_state.editor.read(cx);
265 let cursor = editor.selections.newest_anchor().head();
266 (
267 editor.display_map.clone(),
268 editor.project().cloned(),
269 editor.buffer().clone(),
270 cursor,
271 )
272 };
273 let Some(project) = project else {
274 return;
275 };
276
277 let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
278 let is_singleton = multi_buffer_snapshot.is_singleton();
279 self.is_singleton = is_singleton;
280
281 let mut entries = Vec::new();
282
283 display_map.update(cx, |display_map, cx| {
284 for (key, text_highlights) in display_map.all_text_highlights() {
285 for range in &text_highlights.1 {
286 let excerpt_id = range.start.excerpt_id;
287 let (range_display, sort_key) = format_anchor_range(
288 range,
289 excerpt_id,
290 &multi_buffer_snapshot,
291 is_singleton,
292 );
293 entries.push(HighlightEntry {
294 excerpt_id,
295 range: range.clone(),
296 range_display,
297 style: text_highlights.0,
298 category: HighlightCategory::Text(*key),
299 sort_key,
300 });
301 }
302 }
303
304 project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
305 for (buffer_id, (tokens, interner)) in display_map.all_semantic_token_highlights() {
306 let language_name = multi_buffer
307 .read(cx)
308 .buffer(*buffer_id)
309 .and_then(|buf| buf.read(cx).language().map(|l| l.name()));
310 for token in tokens.iter() {
311 let range = token.range.start..token.range.end;
312 let excerpt_id = range.start.excerpt_id;
313 let (range_display, sort_key) = format_anchor_range(
314 &range,
315 excerpt_id,
316 &multi_buffer_snapshot,
317 is_singleton,
318 );
319 let Some(stylizer) = lsp_store.get_or_create_token_stylizer(
320 token.server_id,
321 language_name.as_ref(),
322 cx,
323 ) else {
324 continue;
325 };
326 entries.push(HighlightEntry {
327 excerpt_id,
328 range,
329 range_display,
330 style: interner[token.style],
331 category: HighlightCategory::SemanticToken {
332 token_type: stylizer.token_type_name(token.token_type).cloned(),
333 token_modifiers: stylizer
334 .token_modifiers(token.token_modifiers)
335 .map(SharedString::from),
336 },
337 sort_key,
338 });
339 }
340 }
341 });
342 });
343
344 entries.sort_by(|a, b| {
345 a.sort_key
346 .cmp(&b.sort_key)
347 .then_with(|| a.category.cmp(&b.category))
348 });
349 entries.dedup_by(|a, b| a.sort_key == b.sort_key && a.category == b.category);
350
351 self.cached_entries = entries;
352 self.rebuild_display_items(&multi_buffer_snapshot, cx);
353
354 if self.skip_next_scroll {
355 self.skip_next_scroll = false;
356 } else {
357 self.scroll_to_cursor_position(&cursor_position, &multi_buffer_snapshot);
358 }
359 cx.notify();
360 }
361
362 fn rebuild_display_items(&mut self, snapshot: &MultiBufferSnapshot, cx: &App) {
363 self.display_items.clear();
364
365 let mut last_excerpt_id: Option<ExcerptId> = None;
366
367 for (entry_ix, entry) in self.cached_entries.iter().enumerate() {
368 if !self.should_show_entry(entry) {
369 continue;
370 }
371
372 if !self.is_singleton {
373 let excerpt_changed =
374 last_excerpt_id.is_none_or(|last_id| last_id != entry.excerpt_id);
375 if excerpt_changed {
376 last_excerpt_id = Some(entry.excerpt_id);
377 let label = excerpt_label_for(entry.excerpt_id, snapshot, cx);
378 self.display_items
379 .push(DisplayItem::ExcerptSeparator { label });
380 }
381 }
382
383 self.display_items.push(DisplayItem::Entry { entry_ix });
384 }
385 }
386
387 fn should_show_entry(&self, entry: &HighlightEntry) -> bool {
388 match entry.category {
389 HighlightCategory::Text(_) => self.show_text_highlights,
390 HighlightCategory::SemanticToken { .. } => self.show_semantic_tokens,
391 }
392 }
393
394 fn scroll_to_cursor_position(&mut self, cursor: &Anchor, snapshot: &MultiBufferSnapshot) {
395 let cursor_point = cursor.to_point(snapshot);
396 let cursor_key = (cursor_point.row, cursor_point.column);
397 let cursor_excerpt = cursor.excerpt_id;
398
399 let best = self
400 .display_items
401 .iter()
402 .enumerate()
403 .filter_map(|(display_ix, item)| match item {
404 DisplayItem::Entry { entry_ix } => {
405 let entry = &self.cached_entries[*entry_ix];
406 Some((display_ix, *entry_ix, entry))
407 }
408 _ => None,
409 })
410 .filter(|(_, _, entry)| {
411 let (excerpt_id, start_row, start_col, end_row, end_col) = entry.sort_key;
412 if !self.is_singleton && excerpt_id != cursor_excerpt {
413 return false;
414 }
415 let start = (start_row, start_col);
416 let end = (end_row, end_col);
417 cursor_key >= start && cursor_key <= end
418 })
419 .min_by_key(|(_, _, entry)| {
420 let (_, start_row, start_col, end_row, end_col) = entry.sort_key;
421 (end_row - start_row, end_col.saturating_sub(start_col))
422 })
423 .map(|(display_ix, entry_ix, _)| (display_ix, entry_ix));
424
425 if let Some((display_ix, entry_ix)) = best {
426 self.selected_item_ix = Some(entry_ix);
427 self.list_scroll_handle
428 .scroll_to_item(display_ix, ScrollStrategy::Center);
429 }
430 }
431
432 fn update_editor_with_range_for_entry(
433 &self,
434 entry_ix: usize,
435 window: &mut Window,
436 cx: &mut Context<Self>,
437 f: &mut dyn FnMut(&mut Editor, Range<Anchor>, usize, &mut Window, &mut Context<Editor>),
438 ) -> Option<()> {
439 let editor_state = self.editor.as_ref()?;
440 let entry = self.cached_entries.get(entry_ix)?;
441 let range = entry.range.clone();
442 let key = cx.entity_id().as_u64() as usize;
443
444 editor_state.editor.update(cx, |editor, cx| {
445 f(editor, range, key, window, cx);
446 });
447 Some(())
448 }
449
450 fn render_entry(&self, entry: &HighlightEntry, selected: bool, cx: &App) -> Div {
451 let colors = cx.theme().colors();
452 let style_preview = render_style_preview(entry.style, selected, cx);
453
454 h_flex()
455 .gap_1()
456 .child(style_preview)
457 .child(Label::new(entry.range_display.clone()).color(Color::Default))
458 .child(
459 Label::new(entry.category.label())
460 .size(LabelSize::Small)
461 .color(Color::Muted),
462 )
463 .text_bg(if selected {
464 colors.element_selected
465 } else {
466 Hsla::default()
467 })
468 .pl(rems(0.5))
469 .hover(|style| style.bg(colors.element_hover))
470 }
471
472 fn render_separator(&self, label: &SharedString, cx: &App) -> Div {
473 let colors = cx.theme().colors();
474 h_flex()
475 .gap_1()
476 .px(rems(0.5))
477 .bg(colors.surface_background)
478 .border_b_1()
479 .border_color(colors.border_variant)
480 .child(
481 Label::new(label.clone())
482 .size(LabelSize::Small)
483 .color(Color::Muted),
484 )
485 }
486
487 fn compute_items(
488 &mut self,
489 visible_range: Range<usize>,
490 _window: &mut Window,
491 cx: &mut Context<Self>,
492 ) -> Vec<Div> {
493 let mut items = Vec::new();
494
495 for display_ix in visible_range {
496 let Some(display_item) = self.display_items.get(display_ix) else {
497 continue;
498 };
499
500 match display_item {
501 DisplayItem::ExcerptSeparator { label } => {
502 items.push(self.render_separator(label, cx));
503 }
504 DisplayItem::Entry { entry_ix } => {
505 let entry_ix = *entry_ix;
506 let entry = &self.cached_entries[entry_ix];
507 let selected = Some(entry_ix) == self.selected_item_ix;
508 let rendered = self
509 .render_entry(entry, selected, cx)
510 .on_mouse_down(
511 MouseButton::Left,
512 cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
513 tree_view.selected_item_ix = Some(entry_ix);
514 tree_view.skip_next_scroll = true;
515 tree_view.update_editor_with_range_for_entry(
516 entry_ix,
517 window,
518 cx,
519 &mut |editor, mut range, _, window, cx| {
520 mem::swap(&mut range.start, &mut range.end);
521 editor.change_selections(
522 SelectionEffects::scroll(Autoscroll::newest()),
523 window,
524 cx,
525 |selections| {
526 selections.select_ranges([range]);
527 },
528 );
529 },
530 );
531 cx.notify();
532 }),
533 )
534 .on_mouse_move(cx.listener(
535 move |tree_view, _: &MouseMoveEvent, window, cx| {
536 if tree_view.hovered_item_ix != Some(entry_ix) {
537 tree_view.hovered_item_ix = Some(entry_ix);
538 tree_view.update_editor_with_range_for_entry(
539 entry_ix,
540 window,
541 cx,
542 &mut |editor, range, key, _, cx| {
543 Self::set_editor_highlights(editor, key, &[range], cx);
544 },
545 );
546 cx.notify();
547 }
548 },
549 ));
550
551 items.push(rendered);
552 }
553 }
554 }
555
556 items
557 }
558
559 fn set_editor_highlights(
560 editor: &mut Editor,
561 key: usize,
562 ranges: &[Range<Anchor>],
563 cx: &mut Context<Editor>,
564 ) {
565 editor.highlight_background(
566 HighlightKey::HighlightsTreeView(key),
567 ranges,
568 |_, theme| theme.colors().editor_document_highlight_write_background,
569 cx,
570 );
571 }
572
573 fn clear_editor_highlights(editor: &Entity<Editor>, cx: &mut Context<Self>) {
574 let highlight_key = HighlightKey::HighlightsTreeView(cx.entity_id().as_u64() as usize);
575 editor.update(cx, |editor, cx| {
576 editor.clear_background_highlights(highlight_key, cx);
577 });
578 }
579
580 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
581 self.move_selection(-1, window, cx);
582 }
583
584 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
585 self.move_selection(1, window, cx);
586 }
587
588 fn move_selection(&mut self, delta: i32, window: &mut Window, cx: &mut Context<Self>) {
589 if self.display_items.is_empty() {
590 return;
591 }
592
593 let entry_display_items: Vec<(usize, usize)> = self
594 .display_items
595 .iter()
596 .enumerate()
597 .filter_map(|(display_ix, item)| match item {
598 DisplayItem::Entry { entry_ix } => Some((display_ix, *entry_ix)),
599 _ => None,
600 })
601 .collect();
602
603 if entry_display_items.is_empty() {
604 return;
605 }
606
607 let current_pos = self
608 .selected_item_ix
609 .and_then(|selected| {
610 entry_display_items
611 .iter()
612 .position(|(_, entry_ix)| *entry_ix == selected)
613 })
614 .unwrap_or(0);
615
616 let new_pos = if delta < 0 {
617 current_pos.saturating_sub((-delta) as usize)
618 } else {
619 (current_pos + delta as usize).min(entry_display_items.len() - 1)
620 };
621
622 if let Some(&(display_ix, entry_ix)) = entry_display_items.get(new_pos) {
623 self.selected_item_ix = Some(entry_ix);
624 self.skip_next_scroll = true;
625 self.list_scroll_handle
626 .scroll_to_item(display_ix, ScrollStrategy::Center);
627
628 self.update_editor_with_range_for_entry(
629 entry_ix,
630 window,
631 cx,
632 &mut |editor, mut range, _, window, cx| {
633 mem::swap(&mut range.start, &mut range.end);
634 editor.change_selections(
635 SelectionEffects::scroll(Autoscroll::newest()),
636 window,
637 cx,
638 |selections| {
639 selections.select_ranges([range]);
640 },
641 );
642 },
643 );
644
645 cx.notify();
646 }
647 }
648
649 fn entry_count(&self) -> usize {
650 self.cached_entries
651 .iter()
652 .filter(|entry| self.should_show_entry(entry))
653 .count()
654 }
655}
656
657impl Render for HighlightsTreeView {
658 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
659 let display_count = self.display_items.len();
660
661 div()
662 .flex_1()
663 .track_focus(&self.focus_handle)
664 .key_context("HighlightsTreeView")
665 .on_action(cx.listener(Self::select_previous))
666 .on_action(cx.listener(Self::select_next))
667 .bg(cx.theme().colors().editor_background)
668 .map(|this| {
669 if display_count > 0 {
670 this.child(
671 uniform_list(
672 "HighlightsTreeView",
673 display_count,
674 cx.processor(move |this, range: Range<usize>, window, cx| {
675 this.compute_items(range, window, cx)
676 }),
677 )
678 .size_full()
679 .track_scroll(&self.list_scroll_handle)
680 .text_bg(cx.theme().colors().background)
681 .into_any_element(),
682 )
683 .vertical_scrollbar_for(&self.list_scroll_handle, window, cx)
684 .into_any_element()
685 } else {
686 let inner_content = v_flex()
687 .items_center()
688 .text_center()
689 .gap_2()
690 .max_w_3_5()
691 .map(|this| {
692 if self.editor.is_some() {
693 let has_any = !self.cached_entries.is_empty();
694 if has_any {
695 this.child(Label::new("All highlights are filtered out"))
696 .child(
697 Label::new(
698 "Enable text or semantic highlights in the toolbar",
699 )
700 .size(LabelSize::Small),
701 )
702 } else {
703 this.child(Label::new("No highlights found")).child(
704 Label::new(
705 "The editor has no text or semantic token highlights",
706 )
707 .size(LabelSize::Small),
708 )
709 }
710 } else {
711 this.child(Label::new("Not attached to an editor")).child(
712 Label::new("Focus an editor to show highlights")
713 .size(LabelSize::Small),
714 )
715 }
716 });
717
718 this.h_flex()
719 .size_full()
720 .justify_center()
721 .child(inner_content)
722 .into_any_element()
723 }
724 })
725 }
726}
727
728impl EventEmitter<()> for HighlightsTreeView {}
729
730impl Focusable for HighlightsTreeView {
731 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
732 self.focus_handle.clone()
733 }
734}
735
736impl Item for HighlightsTreeView {
737 type Event = ();
738
739 fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}
740
741 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
742 "Highlights".into()
743 }
744
745 fn telemetry_event_text(&self) -> Option<&'static str> {
746 None
747 }
748
749 fn can_split(&self) -> bool {
750 true
751 }
752
753 fn clone_on_split(
754 &self,
755 _: Option<workspace::WorkspaceId>,
756 window: &mut Window,
757 cx: &mut Context<Self>,
758 ) -> Task<Option<Entity<Self>>>
759 where
760 Self: Sized,
761 {
762 Task::ready(Some(cx.new(|cx| {
763 let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
764 clone.show_text_highlights = self.show_text_highlights;
765 clone.show_semantic_tokens = self.show_semantic_tokens;
766 clone.skip_next_scroll = false;
767 if let Some(editor) = &self.editor {
768 clone.set_editor(editor.editor.clone(), window, cx)
769 }
770 clone
771 })))
772 }
773
774 fn on_removed(&self, cx: &mut Context<Self>) {
775 if let Some(state) = self.editor.as_ref() {
776 Self::clear_editor_highlights(&state.editor, cx);
777 }
778 }
779}
780
781impl Default for HighlightsTreeToolbarItemView {
782 fn default() -> Self {
783 Self::new()
784 }
785}
786
787impl HighlightsTreeToolbarItemView {
788 pub fn new() -> Self {
789 Self {
790 tree_view: None,
791 _subscription: None,
792 toggle_settings_handle: PopoverMenuHandle::default(),
793 }
794 }
795
796 fn render_header(&self, cx: &Context<Self>) -> Option<ButtonLike> {
797 let tree_view = self.tree_view.as_ref()?;
798 let tree_view = tree_view.read(cx);
799
800 let total = tree_view.cached_entries.len();
801 let filtered = tree_view.entry_count();
802
803 let label = if filtered == total {
804 format!("{} highlights", total)
805 } else {
806 format!("{} / {} highlights", filtered, total)
807 };
808
809 Some(ButtonLike::new("highlights header").child(Label::new(label)))
810 }
811
812 fn render_settings_button(&self, cx: &Context<Self>) -> PopoverMenu<ContextMenu> {
813 let (show_text, show_semantic) = self
814 .tree_view
815 .as_ref()
816 .map(|view| {
817 let v = view.read(cx);
818 (v.show_text_highlights, v.show_semantic_tokens)
819 })
820 .unwrap_or((true, true));
821
822 let tree_view = self.tree_view.as_ref().map(|v| v.downgrade());
823
824 PopoverMenu::new("highlights-tree-settings")
825 .trigger_with_tooltip(
826 IconButton::new("toggle-highlights-settings-icon", IconName::Sliders)
827 .icon_size(IconSize::Small)
828 .style(ButtonStyle::Subtle)
829 .toggle_state(self.toggle_settings_handle.is_deployed()),
830 Tooltip::text("Highlights Settings"),
831 )
832 .anchor(Corner::TopRight)
833 .with_handle(self.toggle_settings_handle.clone())
834 .menu(move |window, cx| {
835 let tree_view_for_text = tree_view.clone();
836 let tree_view_for_semantic = tree_view.clone();
837
838 let menu = ContextMenu::build(window, cx, move |menu, _, _| {
839 menu.toggleable_entry(
840 "Text Highlights",
841 show_text,
842 IconPosition::Start,
843 Some(ToggleTextHighlights.boxed_clone()),
844 {
845 let tree_view = tree_view_for_text.clone();
846 move |_, cx| {
847 if let Some(view) = tree_view.as_ref() {
848 view.update(cx, |view, cx| {
849 view.show_text_highlights = !view.show_text_highlights;
850 let snapshot = view.editor.as_ref().map(|s| {
851 s.editor.read(cx).buffer().read(cx).snapshot(cx)
852 });
853 if let Some(snapshot) = snapshot {
854 view.rebuild_display_items(&snapshot, cx);
855 }
856 cx.notify();
857 })
858 .ok();
859 }
860 }
861 },
862 )
863 .toggleable_entry(
864 "Semantic Tokens",
865 show_semantic,
866 IconPosition::Start,
867 Some(ToggleSemanticTokens.boxed_clone()),
868 {
869 move |_, cx| {
870 if let Some(view) = tree_view_for_semantic.as_ref() {
871 view.update(cx, |view, cx| {
872 view.show_semantic_tokens = !view.show_semantic_tokens;
873 let snapshot = view.editor.as_ref().map(|s| {
874 s.editor.read(cx).buffer().read(cx).snapshot(cx)
875 });
876 if let Some(snapshot) = snapshot {
877 view.rebuild_display_items(&snapshot, cx);
878 }
879 cx.notify();
880 })
881 .ok();
882 }
883 }
884 },
885 )
886 });
887
888 Some(menu)
889 })
890 }
891}
892
893impl Render for HighlightsTreeToolbarItemView {
894 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
895 h_flex()
896 .gap_1()
897 .children(self.render_header(cx))
898 .child(self.render_settings_button(cx))
899 }
900}
901
902impl EventEmitter<ToolbarItemEvent> for HighlightsTreeToolbarItemView {}
903
904impl ToolbarItemView for HighlightsTreeToolbarItemView {
905 fn set_active_pane_item(
906 &mut self,
907 active_pane_item: Option<&dyn ItemHandle>,
908 window: &mut Window,
909 cx: &mut Context<Self>,
910 ) -> ToolbarItemLocation {
911 if let Some(item) = active_pane_item
912 && let Some(view) = item.downcast::<HighlightsTreeView>()
913 {
914 self.tree_view = Some(view.clone());
915 self._subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
916 return ToolbarItemLocation::PrimaryLeft;
917 }
918 self.tree_view = None;
919 self._subscription = None;
920 ToolbarItemLocation::Hidden
921 }
922}
923
924fn excerpt_label_for(
925 excerpt_id: ExcerptId,
926 snapshot: &MultiBufferSnapshot,
927 cx: &App,
928) -> SharedString {
929 let buffer = snapshot.buffer_for_excerpt(excerpt_id);
930 let path_label = buffer
931 .and_then(|buf| buf.file())
932 .map(|file| {
933 let full_path = file.full_path(cx);
934 full_path.to_string_lossy().to_string()
935 })
936 .unwrap_or_else(|| "untitled".to_string());
937 path_label.into()
938}
939
940fn format_anchor_range(
941 range: &Range<Anchor>,
942 excerpt_id: ExcerptId,
943 snapshot: &MultiBufferSnapshot,
944 is_singleton: bool,
945) -> (SharedString, (ExcerptId, u32, u32, u32, u32)) {
946 if is_singleton {
947 let start = range.start.to_point(snapshot);
948 let end = range.end.to_point(snapshot);
949 let display = SharedString::from(format!(
950 "[{}:{} - {}:{}]",
951 start.row + 1,
952 start.column + 1,
953 end.row + 1,
954 end.column + 1,
955 ));
956 let sort_key = (excerpt_id, start.row, start.column, end.row, end.column);
957 (display, sort_key)
958 } else {
959 let buffer = snapshot.buffer_for_excerpt(excerpt_id);
960 if let Some(buffer) = buffer {
961 let start = language::ToPoint::to_point(&range.start.text_anchor, buffer);
962 let end = language::ToPoint::to_point(&range.end.text_anchor, buffer);
963 let display = SharedString::from(format!(
964 "[{}:{} - {}:{}]",
965 start.row + 1,
966 start.column + 1,
967 end.row + 1,
968 end.column + 1,
969 ));
970 let sort_key = (excerpt_id, start.row, start.column, end.row, end.column);
971 (display, sort_key)
972 } else {
973 let start = range.start.to_point(snapshot);
974 let end = range.end.to_point(snapshot);
975 let display = SharedString::from(format!(
976 "[{}:{} - {}:{}]",
977 start.row + 1,
978 start.column + 1,
979 end.row + 1,
980 end.column + 1,
981 ));
982 let sort_key = (excerpt_id, start.row, start.column, end.row, end.column);
983 (display, sort_key)
984 }
985 }
986}
987
988fn render_style_preview(style: HighlightStyle, selected: bool, cx: &App) -> Div {
989 let colors = cx.theme().colors();
990
991 let display_color = style.color.or(style.background_color);
992
993 let mut preview = div().px_1().rounded_sm();
994
995 if let Some(color) = display_color {
996 if selected {
997 preview = preview.border_1().border_color(color).text_color(color);
998 } else {
999 preview = preview.bg(color);
1000 }
1001 } else {
1002 preview = preview.bg(colors.element_background);
1003 }
1004
1005 let mut parts = Vec::new();
1006
1007 if let Some(color) = display_color {
1008 parts.push(format_hsla_as_hex(color));
1009 }
1010 if style.font_weight.is_some() {
1011 parts.push("bold".to_string());
1012 }
1013 if style.font_style.is_some() {
1014 parts.push("italic".to_string());
1015 }
1016 if style.strikethrough.is_some() {
1017 parts.push("strike".to_string());
1018 }
1019 if style.underline.is_some() {
1020 parts.push("underline".to_string());
1021 }
1022
1023 let label_text = if parts.is_empty() {
1024 "none".to_string()
1025 } else {
1026 parts.join(" ")
1027 };
1028
1029 preview.child(Label::new(label_text).size(LabelSize::Small).when_some(
1030 display_color.filter(|_| selected),
1031 |label, display_color| label.color(Color::Custom(display_color)),
1032 ))
1033}
1034
1035fn format_hsla_as_hex(color: Hsla) -> String {
1036 let rgba = color.to_rgb();
1037 let r = (rgba.r * 255.0).round() as u8;
1038 let g = (rgba.g * 255.0).round() as u8;
1039 let b = (rgba.b * 255.0).round() as u8;
1040 let a = (rgba.a * 255.0).round() as u8;
1041 if a == 255 {
1042 format!("#{:02X}{:02X}{:02X}", r, g, b)
1043 } else {
1044 format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
1045 }
1046}