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