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