highlights_tree_view.rs

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