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