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