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