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 Some(highlight_id) = highlight_maps[capture.grammar_index].get(capture.index)
424 else {
425 continue;
426 };
427 let Some(style) = syntax_theme.get(highlight_id).cloned() else {
428 continue;
429 };
430
431 let theme_key = syntax_theme
432 .get_capture_name(highlight_id)
433 .map(|theme_key| SharedString::from(theme_key.to_string()));
434
435 let capture_name = grammars[capture.grammar_index]
436 .highlights_config
437 .as_ref()
438 .and_then(|config| config.query.capture_names().get(capture.index as usize))
439 .map(|capture_name| SharedString::from((*capture_name).to_string()))
440 .unwrap_or_else(|| SharedString::from("unknown"));
441
442 let start_anchor = buffer_snapshot.anchor_before(capture.node.start_byte());
443 let end_anchor = buffer_snapshot.anchor_after(capture.node.end_byte());
444
445 let start = multi_buffer_snapshot.anchor_in_excerpt(start_anchor);
446 let end = multi_buffer_snapshot.anchor_in_excerpt(end_anchor);
447
448 let (start, end) = match (start, end) {
449 (Some(s), Some(e)) => (s, e),
450 _ => continue,
451 };
452
453 let range = start..end;
454 let Some((range_display, buffer_id, buffer_point_range)) =
455 format_anchor_range(&range, &multi_buffer_snapshot)
456 else {
457 continue;
458 };
459
460 entries.push(HighlightEntry {
461 range,
462 buffer_id,
463 range_display,
464 style,
465 category: HighlightCategory::SyntaxToken {
466 capture_name,
467 theme_key,
468 },
469 buffer_point_range,
470 });
471 }
472 }
473
474 entries.sort_by(|a, b| {
475 a.buffer_id
476 .cmp(&b.buffer_id)
477 .then_with(|| a.buffer_point_range.start.cmp(&b.buffer_point_range.start))
478 .then_with(|| a.buffer_point_range.end.cmp(&b.buffer_point_range.end))
479 .then_with(|| a.category.cmp(&b.category))
480 });
481 entries.dedup_by(|a, b| {
482 a.buffer_id == b.buffer_id
483 && a.buffer_point_range == b.buffer_point_range
484 && a.category == b.category
485 });
486
487 self.cached_entries = entries;
488 self.rebuild_display_items(&multi_buffer_snapshot, cx);
489
490 if self.skip_next_scroll {
491 self.skip_next_scroll = false;
492 } else {
493 self.scroll_to_cursor_position(&cursor_position, &multi_buffer_snapshot);
494 }
495 cx.notify();
496 }
497
498 fn rebuild_display_items(&mut self, snapshot: &MultiBufferSnapshot, cx: &App) {
499 self.display_items.clear();
500
501 let mut last_range_end: Option<Anchor> = None;
502
503 for (entry_ix, entry) in self.cached_entries.iter().enumerate() {
504 if !self.should_show_entry(entry) {
505 continue;
506 }
507
508 if !self.is_singleton {
509 let excerpt_changed = last_range_end.is_none_or(|anchor| {
510 snapshot
511 .excerpt_containing(anchor..entry.range.start)
512 .is_none()
513 });
514 if excerpt_changed {
515 last_range_end = Some(entry.range.end);
516 let label = excerpt_label_for(entry, snapshot, cx);
517 self.display_items
518 .push(DisplayItem::ExcerptSeparator { label });
519 }
520 }
521
522 self.display_items.push(DisplayItem::Entry { entry_ix });
523 }
524 }
525
526 fn should_show_entry(&self, entry: &HighlightEntry) -> bool {
527 match entry.category {
528 HighlightCategory::Text(_) => self.show_text_highlights,
529 HighlightCategory::SyntaxToken { .. } => self.show_syntax_tokens,
530 HighlightCategory::SemanticToken { .. } => self.show_semantic_tokens,
531 }
532 }
533
534 fn scroll_to_cursor_position(&mut self, cursor: &Anchor, snapshot: &MultiBufferSnapshot) {
535 let best = self
536 .display_items
537 .iter()
538 .enumerate()
539 .filter_map(|(display_ix, item)| match item {
540 DisplayItem::Entry { entry_ix } => {
541 let entry = &self.cached_entries[*entry_ix];
542 Some((display_ix, *entry_ix, entry))
543 }
544 _ => None,
545 })
546 .filter(|(_, _, entry)| {
547 entry.range.start.cmp(&cursor, snapshot).is_le()
548 && cursor.cmp(&entry.range.end, snapshot).is_lt()
549 })
550 .min_by_key(|(_, _, entry)| {
551 (
552 entry.buffer_point_range.end.row - entry.buffer_point_range.start.row,
553 entry
554 .buffer_point_range
555 .end
556 .column
557 .saturating_sub(entry.buffer_point_range.start.column),
558 )
559 })
560 .map(|(display_ix, entry_ix, _)| (display_ix, entry_ix));
561
562 if let Some((display_ix, entry_ix)) = best {
563 self.selected_item_ix = Some(entry_ix);
564 self.list_scroll_handle
565 .scroll_to_item(display_ix, ScrollStrategy::Center);
566 }
567 }
568
569 fn update_editor_with_range_for_entry(
570 &self,
571 entry_ix: usize,
572 window: &mut Window,
573 cx: &mut Context<Self>,
574 f: &mut dyn FnMut(&mut Editor, Range<Anchor>, usize, &mut Window, &mut Context<Editor>),
575 ) -> Option<()> {
576 let editor_state = self.editor.as_ref()?;
577 let entry = self.cached_entries.get(entry_ix)?;
578 let range = entry.range.clone();
579 let key = cx.entity_id().as_u64() as usize;
580
581 editor_state.editor.update(cx, |editor, cx| {
582 f(editor, range, key, window, cx);
583 });
584 Some(())
585 }
586
587 fn render_entry(&self, entry: &HighlightEntry, selected: bool, cx: &App) -> Div {
588 let colors = cx.theme().colors();
589 let style_preview = render_style_preview(entry.style, selected, cx);
590
591 h_flex()
592 .gap_1()
593 .child(style_preview)
594 .child(Label::new(entry.range_display.clone()).color(Color::Default))
595 .child(
596 Label::new(entry.category.label())
597 .size(LabelSize::Small)
598 .color(Color::Muted),
599 )
600 .text_bg(if selected {
601 colors.element_selected
602 } else {
603 Hsla::default()
604 })
605 .pl(rems(0.5))
606 .hover(|style| style.bg(colors.element_hover))
607 }
608
609 fn render_separator(&self, label: &SharedString, cx: &App) -> Div {
610 let colors = cx.theme().colors();
611 h_flex()
612 .gap_1()
613 .px(rems(0.5))
614 .bg(colors.surface_background)
615 .border_b_1()
616 .border_color(colors.border_variant)
617 .child(
618 Label::new(label.clone())
619 .size(LabelSize::Small)
620 .color(Color::Muted),
621 )
622 }
623
624 fn compute_items(
625 &mut self,
626 visible_range: Range<usize>,
627 _window: &mut Window,
628 cx: &mut Context<Self>,
629 ) -> Vec<Div> {
630 let mut items = Vec::new();
631
632 for display_ix in visible_range {
633 let Some(display_item) = self.display_items.get(display_ix) else {
634 continue;
635 };
636
637 match display_item {
638 DisplayItem::ExcerptSeparator { label } => {
639 items.push(self.render_separator(label, cx));
640 }
641 DisplayItem::Entry { entry_ix } => {
642 let entry_ix = *entry_ix;
643 let entry = &self.cached_entries[entry_ix];
644 let selected = Some(entry_ix) == self.selected_item_ix;
645 let rendered = self
646 .render_entry(entry, selected, cx)
647 .on_mouse_down(
648 MouseButton::Left,
649 cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
650 tree_view.selected_item_ix = Some(entry_ix);
651 tree_view.skip_next_scroll = true;
652 tree_view.update_editor_with_range_for_entry(
653 entry_ix,
654 window,
655 cx,
656 &mut |editor, mut range, _, window, cx| {
657 mem::swap(&mut range.start, &mut range.end);
658 editor.change_selections(
659 SelectionEffects::scroll(Autoscroll::newest()),
660 window,
661 cx,
662 |selections| {
663 selections.select_ranges([range]);
664 },
665 );
666 },
667 );
668 cx.notify();
669 }),
670 )
671 .on_mouse_move(cx.listener(
672 move |tree_view, _: &MouseMoveEvent, window, cx| {
673 if tree_view.hovered_item_ix != Some(entry_ix) {
674 tree_view.hovered_item_ix = Some(entry_ix);
675 tree_view.update_editor_with_range_for_entry(
676 entry_ix,
677 window,
678 cx,
679 &mut |editor, range, key, _, cx| {
680 Self::set_editor_highlights(editor, key, &[range], cx);
681 },
682 );
683 cx.notify();
684 }
685 },
686 ));
687
688 items.push(rendered);
689 }
690 }
691 }
692
693 items
694 }
695
696 fn set_editor_highlights(
697 editor: &mut Editor,
698 key: usize,
699 ranges: &[Range<Anchor>],
700 cx: &mut Context<Editor>,
701 ) {
702 editor.highlight_background(
703 HighlightKey::HighlightsTreeView(key),
704 ranges,
705 |_, theme| theme.colors().editor_document_highlight_write_background,
706 cx,
707 );
708 }
709
710 fn clear_editor_highlights(editor: &Entity<Editor>, cx: &mut Context<Self>) {
711 let highlight_key = HighlightKey::HighlightsTreeView(cx.entity_id().as_u64() as usize);
712 editor.update(cx, |editor, cx| {
713 editor.clear_background_highlights(highlight_key, cx);
714 });
715 }
716
717 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
718 self.move_selection(-1, window, cx);
719 }
720
721 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
722 self.move_selection(1, window, cx);
723 }
724
725 fn move_selection(&mut self, delta: i32, window: &mut Window, cx: &mut Context<Self>) {
726 if self.display_items.is_empty() {
727 return;
728 }
729
730 let entry_display_items: Vec<(usize, usize)> = self
731 .display_items
732 .iter()
733 .enumerate()
734 .filter_map(|(display_ix, item)| match item {
735 DisplayItem::Entry { entry_ix } => Some((display_ix, *entry_ix)),
736 _ => None,
737 })
738 .collect();
739
740 if entry_display_items.is_empty() {
741 return;
742 }
743
744 let current_pos = self
745 .selected_item_ix
746 .and_then(|selected| {
747 entry_display_items
748 .iter()
749 .position(|(_, entry_ix)| *entry_ix == selected)
750 })
751 .unwrap_or(0);
752
753 let new_pos = if delta < 0 {
754 current_pos.saturating_sub((-delta) as usize)
755 } else {
756 (current_pos + delta as usize).min(entry_display_items.len() - 1)
757 };
758
759 if let Some(&(display_ix, entry_ix)) = entry_display_items.get(new_pos) {
760 self.selected_item_ix = Some(entry_ix);
761 self.skip_next_scroll = true;
762 self.list_scroll_handle
763 .scroll_to_item(display_ix, ScrollStrategy::Center);
764
765 self.update_editor_with_range_for_entry(
766 entry_ix,
767 window,
768 cx,
769 &mut |editor, mut range, _, window, cx| {
770 mem::swap(&mut range.start, &mut range.end);
771 editor.change_selections(
772 SelectionEffects::scroll(Autoscroll::newest()),
773 window,
774 cx,
775 |selections| {
776 selections.select_ranges([range]);
777 },
778 );
779 },
780 );
781
782 cx.notify();
783 }
784 }
785
786 fn entry_count(&self) -> usize {
787 self.cached_entries
788 .iter()
789 .filter(|entry| self.should_show_entry(entry))
790 .count()
791 }
792}
793
794impl Render for HighlightsTreeView {
795 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
796 let display_count = self.display_items.len();
797
798 div()
799 .flex_1()
800 .track_focus(&self.focus_handle)
801 .key_context("HighlightsTreeView")
802 .on_action(cx.listener(Self::select_previous))
803 .on_action(cx.listener(Self::select_next))
804 .bg(cx.theme().colors().editor_background)
805 .map(|this| {
806 if display_count > 0 {
807 this.child(
808 uniform_list(
809 "HighlightsTreeView",
810 display_count,
811 cx.processor(move |this, range: Range<usize>, window, cx| {
812 this.compute_items(range, window, cx)
813 }),
814 )
815 .size_full()
816 .track_scroll(&self.list_scroll_handle)
817 .text_bg(cx.theme().colors().background)
818 .into_any_element(),
819 )
820 .vertical_scrollbar_for(&self.list_scroll_handle, window, cx)
821 .into_any_element()
822 } else {
823 let inner_content = v_flex()
824 .items_center()
825 .text_center()
826 .gap_2()
827 .max_w_3_5()
828 .map(|this| {
829 if self.editor.is_some() {
830 let has_any = !self.cached_entries.is_empty();
831 if has_any {
832 this.child(Label::new("All highlights are filtered out"))
833 .child(
834 Label::new(
835 "Enable text, syntax, or semantic highlights in the toolbar",
836 )
837 .size(LabelSize::Small),
838 )
839 } else {
840 this.child(Label::new("No highlights found")).child(
841 Label::new(
842 "The editor has no text, syntax, or semantic token highlights",
843 )
844 .size(LabelSize::Small),
845 )
846 }
847 } else {
848 this.child(Label::new("Not attached to an editor")).child(
849 Label::new("Focus an editor to show highlights")
850 .size(LabelSize::Small),
851 )
852 }
853 });
854
855 this.h_flex()
856 .size_full()
857 .justify_center()
858 .child(inner_content)
859 .into_any_element()
860 }
861 })
862 }
863}
864
865impl EventEmitter<()> for HighlightsTreeView {}
866
867impl Focusable for HighlightsTreeView {
868 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
869 self.focus_handle.clone()
870 }
871}
872
873impl Item for HighlightsTreeView {
874 type Event = ();
875
876 fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}
877
878 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
879 "Highlights".into()
880 }
881
882 fn telemetry_event_text(&self) -> Option<&'static str> {
883 None
884 }
885
886 fn can_split(&self) -> bool {
887 true
888 }
889
890 fn clone_on_split(
891 &self,
892 _: Option<workspace::WorkspaceId>,
893 window: &mut Window,
894 cx: &mut Context<Self>,
895 ) -> Task<Option<Entity<Self>>>
896 where
897 Self: Sized,
898 {
899 Task::ready(Some(cx.new(|cx| {
900 let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
901 clone.show_text_highlights = self.show_text_highlights;
902 clone.show_syntax_tokens = self.show_syntax_tokens;
903 clone.show_semantic_tokens = self.show_semantic_tokens;
904 clone.skip_next_scroll = false;
905 if let Some(editor) = &self.editor {
906 clone.set_editor(editor.editor.clone(), window, cx)
907 }
908 clone
909 })))
910 }
911
912 fn on_removed(&self, cx: &mut Context<Self>) {
913 if let Some(state) = self.editor.as_ref() {
914 Self::clear_editor_highlights(&state.editor, cx);
915 }
916 }
917}
918
919impl Default for HighlightsTreeToolbarItemView {
920 fn default() -> Self {
921 Self::new()
922 }
923}
924
925impl HighlightsTreeToolbarItemView {
926 pub fn new() -> Self {
927 Self {
928 tree_view: None,
929 _subscription: None,
930 toggle_settings_handle: PopoverMenuHandle::default(),
931 }
932 }
933
934 fn render_header(&self, cx: &Context<Self>) -> Option<ButtonLike> {
935 let tree_view = self.tree_view.as_ref()?;
936 let tree_view = tree_view.read(cx);
937
938 let total = tree_view.cached_entries.len();
939 let filtered = tree_view.entry_count();
940
941 let label = if filtered == total {
942 format!("{} highlights", total)
943 } else {
944 format!("{} / {} highlights", filtered, total)
945 };
946
947 Some(ButtonLike::new("highlights header").child(Label::new(label)))
948 }
949
950 fn render_settings_button(&self, cx: &Context<Self>) -> PopoverMenu<ContextMenu> {
951 let (show_text, show_syntax, show_semantic) = self
952 .tree_view
953 .as_ref()
954 .map(|view| {
955 let v = view.read(cx);
956 (
957 v.show_text_highlights,
958 v.show_syntax_tokens,
959 v.show_semantic_tokens,
960 )
961 })
962 .unwrap_or((true, true, true));
963
964 let tree_view = self.tree_view.as_ref().map(|v| v.downgrade());
965
966 PopoverMenu::new("highlights-tree-settings")
967 .trigger_with_tooltip(
968 IconButton::new("toggle-highlights-settings-icon", IconName::Sliders)
969 .icon_size(IconSize::Small)
970 .style(ButtonStyle::Subtle)
971 .toggle_state(self.toggle_settings_handle.is_deployed()),
972 Tooltip::text("Highlights Settings"),
973 )
974 .anchor(Corner::TopRight)
975 .with_handle(self.toggle_settings_handle.clone())
976 .menu(move |window, cx| {
977 let tree_view_for_text = tree_view.clone();
978 let tree_view_for_syntax = tree_view.clone();
979 let tree_view_for_semantic = tree_view.clone();
980
981 let menu = ContextMenu::build(window, cx, move |menu, _, _| {
982 menu.toggleable_entry(
983 "Text Highlights",
984 show_text,
985 IconPosition::Start,
986 Some(ToggleTextHighlights.boxed_clone()),
987 {
988 let tree_view = tree_view_for_text.clone();
989 move |_, cx| {
990 if let Some(view) = tree_view.as_ref() {
991 view.update(cx, |view, cx| {
992 view.show_text_highlights = !view.show_text_highlights;
993 let snapshot = view.editor.as_ref().map(|s| {
994 s.editor.read(cx).buffer().read(cx).snapshot(cx)
995 });
996 if let Some(snapshot) = snapshot {
997 view.rebuild_display_items(&snapshot, cx);
998 }
999 cx.notify();
1000 })
1001 .ok();
1002 }
1003 }
1004 },
1005 )
1006 .toggleable_entry(
1007 "Syntax Tokens",
1008 show_syntax,
1009 IconPosition::Start,
1010 Some(ToggleSyntaxTokens.boxed_clone()),
1011 {
1012 let tree_view = tree_view_for_syntax.clone();
1013 move |_, cx| {
1014 if let Some(view) = tree_view.as_ref() {
1015 view.update(cx, |view, cx| {
1016 view.show_syntax_tokens = !view.show_syntax_tokens;
1017 let snapshot = view.editor.as_ref().map(|s| {
1018 s.editor.read(cx).buffer().read(cx).snapshot(cx)
1019 });
1020 if let Some(snapshot) = snapshot {
1021 view.rebuild_display_items(&snapshot, cx);
1022 }
1023 cx.notify();
1024 })
1025 .ok();
1026 }
1027 }
1028 },
1029 )
1030 .toggleable_entry(
1031 "Semantic Tokens",
1032 show_semantic,
1033 IconPosition::Start,
1034 Some(ToggleSemanticTokens.boxed_clone()),
1035 {
1036 move |_, cx| {
1037 if let Some(view) = tree_view_for_semantic.as_ref() {
1038 view.update(cx, |view, cx| {
1039 view.show_semantic_tokens = !view.show_semantic_tokens;
1040 let snapshot = view.editor.as_ref().map(|s| {
1041 s.editor.read(cx).buffer().read(cx).snapshot(cx)
1042 });
1043 if let Some(snapshot) = snapshot {
1044 view.rebuild_display_items(&snapshot, cx);
1045 }
1046 cx.notify();
1047 })
1048 .ok();
1049 }
1050 }
1051 },
1052 )
1053 });
1054
1055 Some(menu)
1056 })
1057 }
1058}
1059
1060impl Render for HighlightsTreeToolbarItemView {
1061 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1062 h_flex()
1063 .gap_1()
1064 .children(self.render_header(cx))
1065 .child(self.render_settings_button(cx))
1066 }
1067}
1068
1069impl EventEmitter<ToolbarItemEvent> for HighlightsTreeToolbarItemView {}
1070
1071impl ToolbarItemView for HighlightsTreeToolbarItemView {
1072 fn set_active_pane_item(
1073 &mut self,
1074 active_pane_item: Option<&dyn ItemHandle>,
1075 window: &mut Window,
1076 cx: &mut Context<Self>,
1077 ) -> ToolbarItemLocation {
1078 if let Some(item) = active_pane_item
1079 && let Some(view) = item.downcast::<HighlightsTreeView>()
1080 {
1081 self.tree_view = Some(view.clone());
1082 self._subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
1083 return ToolbarItemLocation::PrimaryLeft;
1084 }
1085 self.tree_view = None;
1086 self._subscription = None;
1087 ToolbarItemLocation::Hidden
1088 }
1089}
1090
1091fn excerpt_label_for(
1092 entry: &HighlightEntry,
1093 snapshot: &MultiBufferSnapshot,
1094 cx: &App,
1095) -> SharedString {
1096 let path_label = snapshot
1097 .anchor_to_buffer_anchor(entry.range.start)
1098 .and_then(|(anchor, _)| snapshot.buffer_for_id(anchor.buffer_id))
1099 .and_then(|buf| buf.file())
1100 .map(|file| {
1101 let full_path = file.full_path(cx);
1102 full_path.to_string_lossy().to_string()
1103 })
1104 .unwrap_or_else(|| "untitled".to_string());
1105 path_label.into()
1106}
1107
1108fn format_anchor_range(
1109 range: &Range<Anchor>,
1110 snapshot: &MultiBufferSnapshot,
1111) -> Option<(SharedString, BufferId, Range<Point>)> {
1112 let start = range.start.to_point(snapshot);
1113 let end = range.end.to_point(snapshot);
1114 let ((start_buffer, start), (_, end)) = snapshot
1115 .point_to_buffer_point(start)
1116 .zip(snapshot.point_to_buffer_point(end))?;
1117 let display = SharedString::from(format!(
1118 "[{}:{} - {}:{}]",
1119 start.row + 1,
1120 start.column + 1,
1121 end.row + 1,
1122 end.column + 1,
1123 ));
1124 Some((display, start_buffer.remote_id(), start..end))
1125}
1126
1127fn render_style_preview(style: HighlightStyle, selected: bool, cx: &App) -> Div {
1128 let colors = cx.theme().colors();
1129
1130 let display_color = style.color.or(style.background_color);
1131
1132 let mut preview = div().px_1().rounded_sm();
1133
1134 if let Some(color) = display_color {
1135 if selected {
1136 preview = preview.border_1().border_color(color).text_color(color);
1137 } else {
1138 preview = preview.bg(color);
1139 }
1140 } else {
1141 preview = preview.bg(colors.element_background);
1142 }
1143
1144 let mut parts = Vec::new();
1145
1146 if let Some(color) = display_color {
1147 parts.push(format_hsla_as_hex(color));
1148 }
1149 if style.font_weight.is_some() {
1150 parts.push("bold".to_string());
1151 }
1152 if style.font_style.is_some() {
1153 parts.push("italic".to_string());
1154 }
1155 if style.strikethrough.is_some() {
1156 parts.push("strike".to_string());
1157 }
1158 if style.underline.is_some() {
1159 parts.push("underline".to_string());
1160 }
1161
1162 let label_text = if parts.is_empty() {
1163 "none".to_string()
1164 } else {
1165 parts.join(" ")
1166 };
1167
1168 preview.child(Label::new(label_text).size(LabelSize::Small).when_some(
1169 display_color.filter(|_| selected),
1170 |label, display_color| label.color(Color::Custom(display_color)),
1171 ))
1172}
1173
1174fn format_hsla_as_hex(color: Hsla) -> String {
1175 let rgba = color.to_rgb();
1176 let r = (rgba.r * 255.0).round() as u8;
1177 let g = (rgba.g * 255.0).round() as u8;
1178 let b = (rgba.b * 255.0).round() as u8;
1179 let a = (rgba.a * 255.0).round() as u8;
1180 if a == 255 {
1181 format!("#{:02X}{:02X}{:02X}", r, g, b)
1182 } else {
1183 format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
1184 }
1185}