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