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