highlights_tree_view.rs

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