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