1156 lines · 43.1 KB · Rust
RawDownload
use std::ops::Range; use collections::HashMap;use futures::FutureExt;use futures::future::join_all;use gpui::{App, Context, HighlightStyle, Task};use itertools::Itertools as _;use language::language_settings::LanguageSettings;use language::{Buffer, OutlineItem};use multi_buffer::{    Anchor, AnchorRangeExt as _, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot,    ToOffset as _,};use text::BufferId;use theme::{ActiveTheme as _, SyntaxTheme};use unicode_segmentation::UnicodeSegmentation as _;use util::maybe; use crate::display_map::DisplaySnapshot;use crate::{Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT}; impl Editor {    /// Returns all document outline items for a buffer, using LSP or    /// tree-sitter based on the `document_symbols` setting.    /// External consumers (outline modal, outline panel, breadcrumbs) should use this.    pub fn buffer_outline_items(        &self,        buffer_id: BufferId,        cx: &mut Context<Self>,    ) -> Task<Vec<OutlineItem<text::Anchor>>> {        let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {            return Task::ready(Vec::new());        };         if lsp_symbols_enabled(buffer.read(cx), cx) {            let refresh_task = self.refresh_document_symbols_task.clone();            cx.spawn(async move |editor, cx| {                refresh_task.await;                editor                    .read_with(cx, |editor, _| {                        editor                            .lsp_document_symbols                            .get(&buffer_id)                            .cloned()                            .unwrap_or_default()                    })                    .ok()                    .unwrap_or_default()            })        } else {            let buffer_snapshot = buffer.read(cx).snapshot();            let syntax = cx.theme().syntax().clone();            cx.background_executor()                .spawn(async move { buffer_snapshot.outline(Some(&syntax)).items })        }    }     /// Whether the buffer at `cursor` has LSP document symbols enabled.    pub(super) fn uses_lsp_document_symbols(        &self,        cursor: Anchor,        multi_buffer_snapshot: &MultiBufferSnapshot,        cx: &Context<Self>,    ) -> bool {        let Some((anchor, _)) = multi_buffer_snapshot.anchor_to_buffer_anchor(cursor) else {            return false;        };        let Some(buffer) = self.buffer.read(cx).buffer(anchor.buffer_id) else {            return false;        };        lsp_symbols_enabled(buffer.read(cx), cx)    }     /// Filters editor-local LSP document symbols to the ancestor chain    /// containing `cursor`. Never triggers an LSP request.    pub(super) fn lsp_symbols_at_cursor(        &self,        cursor: Anchor,        multi_buffer_snapshot: &MultiBufferSnapshot,        _cx: &Context<Self>,    ) -> Option<(BufferId, Vec<OutlineItem<Anchor>>)> {        let (cursor_text_anchor, buffer) = multi_buffer_snapshot.anchor_to_buffer_anchor(cursor)?;        let all_items = self            .lsp_document_symbols            .get(&cursor_text_anchor.buffer_id)?;        if all_items.is_empty() {            return None;        }         let mut symbols = all_items            .iter()            .filter(|item| {                item.range.start.cmp(&cursor_text_anchor, buffer).is_le()                    && item.range.end.cmp(&cursor_text_anchor, buffer).is_ge()            })            .filter_map(|item| {                let range_start = multi_buffer_snapshot.anchor_in_buffer(item.range.start)?;                let range_end = multi_buffer_snapshot.anchor_in_buffer(item.range.end)?;                let source_range_for_text_start =                    multi_buffer_snapshot.anchor_in_buffer(item.source_range_for_text.start)?;                let source_range_for_text_end =                    multi_buffer_snapshot.anchor_in_buffer(item.source_range_for_text.end)?;                Some(OutlineItem {                    depth: item.depth,                    range: range_start..range_end,                    selection_range: multi_buffer_snapshot                        .anchor_in_buffer(item.selection_range.start)?                        ..multi_buffer_snapshot.anchor_in_buffer(item.selection_range.end)?,                    source_range_for_text: source_range_for_text_start..source_range_for_text_end,                    text: item.text.clone(),                    highlight_ranges: item.highlight_ranges.clone(),                    name_ranges: item.name_ranges.clone(),                    body_range: item.body_range.as_ref().and_then(|r| {                        Some(                            multi_buffer_snapshot.anchor_in_buffer(r.start)?                                ..multi_buffer_snapshot.anchor_in_buffer(r.end)?,                        )                    }),                    annotation_range: item.annotation_range.as_ref().and_then(|r| {                        Some(                            multi_buffer_snapshot.anchor_in_buffer(r.start)?                                ..multi_buffer_snapshot.anchor_in_buffer(r.end)?,                        )                    }),                })            })            .collect::<Vec<_>>();         let mut prev_depth = None;        symbols.retain(|item| {            let retain = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth);            prev_depth = Some(item.depth);            retain        });         Some((buffer.remote_id(), symbols))    }     /// Fetches document symbols from the LSP for buffers that have the setting    /// enabled. Called from `update_lsp_data` on edits, server events, etc.    /// When the fetch completes, stores results in `self.lsp_document_symbols`    /// and triggers `refresh_outline_symbols_at_cursor` so breadcrumbs pick up the new data.    pub(super) fn refresh_document_symbols(        &mut self,        for_buffer: Option<BufferId>,        cx: &mut Context<Self>,    ) {        if !self.lsp_data_enabled() {            return;        }        let Some(project) = self.project.as_ref().map(|p| p.downgrade()) else {            return;        };         let buffers_to_query = self            .visible_buffers(cx)            .into_iter()            .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))            .filter_map(|buffer| {                let id = buffer.read(cx).remote_id();                if for_buffer.is_none_or(|target| target == id)                    && lsp_symbols_enabled(buffer.read(cx), cx)                {                    Some(buffer)                } else {                    None                }            })            .unique_by(|buffer| buffer.read(cx).remote_id())            .collect::<Vec<_>>();         let mut symbols_altered = false;        let multi_buffer = self.buffer().clone();        self.lsp_document_symbols.retain(|buffer_id, _| {            let Some(buffer) = multi_buffer.read(cx).buffer(*buffer_id) else {                symbols_altered = true;                return false;            };            let retain = lsp_symbols_enabled(buffer.read(cx), cx);            symbols_altered |= !retain;            retain        });        if symbols_altered {            self.refresh_outline_symbols_at_cursor(cx);        }         if buffers_to_query.is_empty() {            return;        }         self.refresh_document_symbols_task = cx            .spawn(async move |editor, cx| {                cx.background_executor()                    .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)                    .await;                 let Some(tasks) = project                    .update(cx, |project, cx| {                        project.lsp_store().update(cx, |lsp_store, cx| {                            buffers_to_query                                .into_iter()                                .map(|buffer| {                                    let buffer_id = buffer.read(cx).remote_id();                                    let task = lsp_store.fetch_document_symbols(&buffer, cx);                                    async move { (buffer_id, task.await) }                                })                                .collect::<Vec<_>>()                        })                    })                    .ok()                else {                    return;                };                 let results = join_all(tasks).await.into_iter().collect::<HashMap<_, _>>();                editor                    .update(cx, |editor, cx| {                        let syntax = cx.theme().syntax().clone();                        let display_snapshot =                            editor.display_map.update(cx, |map, cx| map.snapshot(cx));                        let mut highlighted_results = results;                        for items in highlighted_results.values_mut() {                            for item in items {                                if let Some(highlights) =                                    highlights_from_buffer(&display_snapshot, &item, &syntax)                                {                                    item.highlight_ranges = highlights;                                }                            }                        }                        editor.lsp_document_symbols.extend(highlighted_results);                        editor.refresh_outline_symbols_at_cursor(cx);                    })                    .ok();            })            .shared();    }} fn lsp_symbols_enabled(buffer: &Buffer, cx: &App) -> bool {    LanguageSettings::for_buffer(buffer, cx)        .document_symbols        .lsp_enabled()} /// Finds where the symbol name appears in the buffer and returns combined/// (tree-sitter + semantic token) highlights for those positions.////// First tries to find the name verbatim near the selection range so that/// complex names (`impl Trait for Type`) get full highlighting. Falls back/// to word-by-word matching for cases like `impl<T> Trait<T> for Type`/// where the LSP name doesn't appear verbatim in the buffer.fn highlights_from_buffer(    display_snapshot: &DisplaySnapshot,    item: &OutlineItem<text::Anchor>,    syntax_theme: &SyntaxTheme,) -> Option<Vec<(Range<usize>, HighlightStyle)>> {    let outline_text = &item.text;    if outline_text.is_empty() {        return None;    }     let multi_buffer_snapshot = display_snapshot.buffer();    let multi_buffer_source_range_anchors =        multi_buffer_snapshot.text_anchors_to_visible_anchors([            item.source_range_for_text.start,            item.source_range_for_text.end,        ]);    let Some(anchor_range) = maybe!({        Some(            (*multi_buffer_source_range_anchors.get(0)?)?                ..(*multi_buffer_source_range_anchors.get(1)?)?,        )    }) else {        return None;    };     let selection_point_range = anchor_range.to_point(multi_buffer_snapshot);    let mut search_start = selection_point_range.start;    search_start.column = 0;    let search_start_offset = search_start.to_offset(&multi_buffer_snapshot);    let mut search_end = selection_point_range.end;    search_end.column = multi_buffer_snapshot.line_len(MultiBufferRow(search_end.row));     let search_text = multi_buffer_snapshot        .text_for_range(search_start..search_end)        .collect::<String>();     let mut outline_text_highlights = Vec::new();    match search_text.find(outline_text.as_str()) {        Some(start_index) => {            let multibuffer_start = search_start_offset + MultiBufferOffset(start_index);            let multibuffer_end = multibuffer_start + MultiBufferOffset(outline_text.len());            outline_text_highlights.extend(                display_snapshot                    .combined_highlights(multibuffer_start..multibuffer_end, syntax_theme),            );        }        None => {            for (outline_text_word_start, outline_word) in outline_text.split_word_bound_indices() {                if let Some(start_index) = search_text.find(outline_word) {                    let multibuffer_start = search_start_offset + MultiBufferOffset(start_index);                    let multibuffer_end = multibuffer_start + MultiBufferOffset(outline_word.len());                    outline_text_highlights.extend(                        display_snapshot                            .combined_highlights(multibuffer_start..multibuffer_end, syntax_theme)                            .into_iter()                            .map(|(range_in_word, style)| {                                (                                    outline_text_word_start + range_in_word.start                                        ..outline_text_word_start + range_in_word.end,                                    style,                                )                            }),                    );                }            }        }    }     if outline_text_highlights.is_empty() {        None    } else {        Some(outline_text_highlights)    }} #[cfg(test)]mod tests {    use std::{        sync::{Arc, atomic},        time::Duration,    };     use futures::StreamExt as _;    use gpui::{App, TestAppContext};    use multi_buffer::ToPoint;    use settings::{DocumentSymbols, SettingsStore};    use text::Point;    use util::path;    use workspace::item::{Item, ItemEvent};    use zed_actions::editor::MoveDown;     use crate::{        Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT,        editor_tests::{init_test, update_test_language_settings},        test::editor_lsp_test_context::EditorLspTestContext,    };     fn outline_symbol_names(editor: &Editor) -> Vec<&str> {        editor            .outline_symbols_at_cursor            .as_ref()            .expect("Should have outline symbols")            .1            .iter()            .map(|s| s.text.as_str())            .collect()    }     fn breadcrumb_texts(editor: &Editor, cx: &App) -> Vec<String> {        editor            .breadcrumbs(cx)            .expect("Should have breadcrumbs")            .0            .into_iter()            .map(|segment| segment.text.to_string())            .collect()    }     fn lsp_range(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> lsp::Range {        lsp::Range {            start: lsp::Position::new(start_line, start_char),            end: lsp::Position::new(end_line, end_char),        }    }     fn nested_symbol(        name: &str,        kind: lsp::SymbolKind,        range: lsp::Range,        selection_range: lsp::Range,        children: Vec<lsp::DocumentSymbol>,    ) -> lsp::DocumentSymbol {        #[allow(deprecated)]        lsp::DocumentSymbol {            name: name.to_string(),            detail: None,            kind,            tags: None,            deprecated: None,            range,            selection_range,            children: if children.is_empty() {                None            } else {                Some(children)            },        }    }     #[gpui::test]    async fn test_lsp_document_symbols_fetches_when_enabled(cx: &mut TestAppContext) {        init_test(cx, |_| {});         update_test_language_settings(cx, &|settings| {            settings.defaults.document_symbols = Some(DocumentSymbols::On);        });         let mut cx = EditorLspTestContext::new_rust(            lsp::ServerCapabilities {                document_symbol_provider: Some(lsp::OneOf::Left(true)),                ..lsp::ServerCapabilities::default()            },            cx,        )        .await;        let mut symbol_request = cx            .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(                move |_, _, _| async move {                    Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![                        nested_symbol(                            "main",                            lsp::SymbolKind::FUNCTION,                            lsp_range(0, 0, 2, 1),                            lsp_range(0, 3, 0, 7),                            Vec::new(),                        ),                    ])))                },            );         cx.set_state("fn maˇin() {\n    let x = 1;\n}\n");        assert!(symbol_request.next().await.is_some());        cx.run_until_parked();         cx.update_editor(|editor, _window, _cx| {            assert_eq!(outline_symbol_names(editor), vec!["fn main"]);        });    }     #[gpui::test]    async fn test_lsp_document_symbols_nested(cx: &mut TestAppContext) {        init_test(cx, |_| {});         update_test_language_settings(cx, &|settings| {            settings.defaults.document_symbols = Some(DocumentSymbols::On);        });         let mut cx = EditorLspTestContext::new_rust(            lsp::ServerCapabilities {                document_symbol_provider: Some(lsp::OneOf::Left(true)),                ..lsp::ServerCapabilities::default()            },            cx,        )        .await;        let mut symbol_request = cx            .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(                move |_, _, _| async move {                    Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![                        nested_symbol(                            "Foo",                            lsp::SymbolKind::STRUCT,                            lsp_range(0, 0, 3, 1),                            lsp_range(0, 7, 0, 10),                            vec![                                nested_symbol(                                    "bar",                                    lsp::SymbolKind::FIELD,                                    lsp_range(1, 4, 1, 13),                                    lsp_range(1, 4, 1, 7),                                    Vec::new(),                                ),                                nested_symbol(                                    "baz",                                    lsp::SymbolKind::FIELD,                                    lsp_range(2, 4, 2, 15),                                    lsp_range(2, 4, 2, 7),                                    Vec::new(),                                ),                            ],                        ),                    ])))                },            );         cx.set_state("struct Foo {\n    baˇr: u32,\n    baz: String,\n}\n");        assert!(symbol_request.next().await.is_some());        cx.run_until_parked();         cx.update_editor(|editor, _window, _cx| {            assert_eq!(                outline_symbol_names(editor),                vec!["struct Foo", "bar"],                "cursor is inside Foo > bar, so we expect the containing chain"            );        });    } 

Lines 1–500 of 1156.