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