highlights_tree_view.rs

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