527 lines · 18.2 KB · Rust
RawDownload
use std::{    borrow::Cow,    ops::{Deref, DerefMut, Range},    path::Path,    sync::Arc,}; use anyhow::Result;use language::{markdown_lang, rust_lang};use multi_buffer::MultiBufferOffset;use serde_json::json; use crate::{Editor, ToPoint};use collections::HashSet;use futures::Future;use futures::stream::StreamExt;use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window};use indoc::indoc;use language::{    BlockCommentConfig, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,    point_to_lsp,};use lsp::{notification, request};use project::Project;use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; pub struct EditorLspTestContext {    pub cx: EditorTestContext,    pub lsp: lsp::FakeLanguageServer,    pub workspace: Entity<Workspace>,    pub buffer_lsp_url: lsp::Uri,} #[cfg(test)]pub(crate) fn git_commit_lang() -> Arc<Language> {    Arc::new(Language::new(        LanguageConfig {            name: "Git Commit".into(),            line_comments: vec!["#".into()],            ..Default::default()        },        None,    ))} impl EditorLspTestContext {    pub async fn new(        language: Language,        capabilities: lsp::ServerCapabilities,        cx: &mut gpui::TestAppContext,    ) -> EditorLspTestContext {        let app_state = cx.update(AppState::test);         cx.update(|cx| {            assets::Assets.load_test_fonts(cx);            crate::init(cx);            workspace::init(app_state.clone(), cx);        });         let file_name = format!(            "file.{}",            language                .path_suffixes()                .first()                .expect("language must have a path suffix for EditorLspTestContext")        );         let project = Project::test(app_state.fs.clone(), [], cx).await;         let language_registry = project.read_with(cx, |project, _| project.languages().clone());        let mut fake_servers = language_registry.register_fake_lsp(            language.name(),            FakeLspAdapter {                capabilities,                ..Default::default()            },        );        language_registry.add(Arc::new(language));         let root = Self::root_path();         app_state            .fs            .as_fake()            .insert_tree(                root,                json!({                    ".git": {},                    "dir": {                        file_name.clone(): ""                    }                }),            )            .await;         let window =            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));         let workspace = window.root(cx).unwrap();         let mut cx = VisualTestContext::from_window(*window.deref(), cx);        project            .update(&mut cx, |project, cx| {                project.find_or_create_worktree(root, true, cx)            })            .await            .unwrap();        cx.read(|cx| {            workspace                .read(cx)                .workspace()                .read(cx)                .worktree_scans_complete(cx)        })        .await;        let file = cx.read(|cx| workspace.read(cx).workspace().file_project_paths(cx)[0].clone());        let item = workspace            .update_in(&mut cx, |workspace, window, cx| {                workspace.workspace().update(cx, |workspace, cx| {                    workspace.open_path(file, None, true, window, cx)                })            })            .await            .expect("Could not open test file");        let editor = cx.update(|_, cx| {            item.act_as::<Editor>(cx)                .expect("Opened test file wasn't an editor")        });        editor.update_in(&mut cx, |editor, window, cx| {            let nav_history = workspace                .read(cx)                .workspace()                .read(cx)                .active_pane()                .read(cx)                .nav_history_for_item(&cx.entity());            editor.set_nav_history(Some(nav_history));            window.focus(&editor.focus_handle(cx), cx)        });         let lsp = fake_servers.next().await.unwrap();         // Ensure the language server is fully registered with the buffer        cx.executor().run_until_parked();         let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());         Self {            cx: EditorTestContext {                cx,                window: window.into(),                editor,                assertion_cx: AssertionContextManager::new(),            },            lsp,            workspace,            buffer_lsp_url: lsp::Uri::from_file_path(root.join("dir").join(file_name)).unwrap(),        }    }     pub async fn new_rust(        capabilities: lsp::ServerCapabilities,        cx: &mut gpui::TestAppContext,    ) -> EditorLspTestContext {        Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await    }     pub async fn new_typescript(        capabilities: lsp::ServerCapabilities,        cx: &mut gpui::TestAppContext,    ) -> EditorLspTestContext {        let mut word_characters: HashSet<char> = Default::default();        word_characters.insert('$');        word_characters.insert('#');        let language = Language::new(            LanguageConfig {                name: "Typescript".into(),                matcher: (LanguageMatcher {                    path_suffixes: vec!["ts".to_string()],                    ..Default::default()                }).into(),                brackets: language::BracketPairConfig {                    pairs: vec![language::BracketPair {                        start: "{".to_string(),                        end: "}".to_string(),                        close: true,                        surround: true,                        newline: true,                    }],                    disabled_scopes_by_bracket_ix: Default::default(),                },                word_characters,                ..Default::default()            },            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),        )        .with_queries(LanguageQueries {            brackets: Some(Cow::from(indoc! {r#"                ("(" @open ")" @close)                ("[" @open "]" @close)                ("{" @open "}" @close)                ("<" @open ">" @close)                ("'" @open "'" @close)                ("`" @open "`" @close)                ("\"" @open "\"" @close)"#})),            indents: Some(Cow::from(indoc! {r#"                [                    (call_expression)                    (assignment_expression)                    (member_expression)                    (lexical_declaration)                    (variable_declaration)                    (assignment_expression)                    (if_statement)                    (for_statement)                ] @indent                 (_ "[" "]" @end) @indent                (_ "<" ">" @end) @indent                (_ "{" "}" @end) @indent                (_ "(" ")" @end) @indent                "#})),            text_objects: Some(Cow::from(indoc! {r#"                (function_declaration                    body: (_                        "{"                        (_)* @function.inside                        "}")) @function.around                 (method_definition                    body: (_                        "{"                        (_)* @function.inside                        "}")) @function.around                 ; Arrow function in variable declaration - capture the full declaration                ([                    (lexical_declaration                        (variable_declarator                            value: (arrow_function                                body: (statement_block                                    "{"                                    (_)* @function.inside                                    "}"))))                    (variable_declaration                        (variable_declarator                            value: (arrow_function                                body: (statement_block                                    "{"                                    (_)* @function.inside                                    "}"))))                ]) @function.around                 ([                    (lexical_declaration                        (variable_declarator                            value: (arrow_function)))                    (variable_declaration                        (variable_declarator                            value: (arrow_function)))                ]) @function.around                 ; Catch-all for arrow functions in other contexts (callbacks, etc.)                ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))                "#})),            ..Default::default()        })        .expect("Could not parse queries");         Self::new(language, capabilities, cx).await    }     pub async fn new_tsx(        capabilities: lsp::ServerCapabilities,        cx: &mut gpui::TestAppContext,    ) -> EditorLspTestContext {        let mut word_characters: HashSet<char> = Default::default();        word_characters.insert('$');        word_characters.insert('#');        let language = Language::new(            LanguageConfig {                name: "TSX".into(),                matcher: (LanguageMatcher {                    path_suffixes: vec!["tsx".to_string()],                    ..Default::default()                }).into(),                brackets: language::BracketPairConfig {                    pairs: vec![language::BracketPair {                        start: "{".to_string(),                        end: "}".to_string(),                        close: true,                        surround: true,                        newline: true,                    }],                    disabled_scopes_by_bracket_ix: Default::default(),                },                word_characters,                ..Default::default()            },            Some(tree_sitter_typescript::LANGUAGE_TSX.into()),        )        .with_queries(LanguageQueries {            brackets: Some(Cow::from(indoc! {r#"                ("(" @open ")" @close)                ("[" @open "]" @close)                ("{" @open "}" @close)                ("<" @open ">" @close)                ("<" @open "/>" @close)                ("</" @open ">" @close)                ("\"" @open "\"" @close)                ("'" @open "'" @close)                ("`" @open "`" @close)                ((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))"#})),            indents: Some(Cow::from(indoc! {r#"                [                    (call_expression)                    (assignment_expression)                    (member_expression)                    (lexical_declaration)                    (variable_declaration)                    (assignment_expression)                    (if_statement)                    (for_statement)                ] @indent                 (_ "[" "]" @end) @indent                (_ "<" ">" @end) @indent                (_ "{" "}" @end) @indent                (_ "(" ")" @end) @indent                 (jsx_opening_element ">" @end) @indent                 (jsx_element                  (jsx_opening_element) @start                  (jsx_closing_element)? @end) @indent                "#})),            text_objects: Some(Cow::from(indoc! {r#"                (function_declaration                    body: (_                        "{"                        (_)* @function.inside                        "}")) @function.around                 (method_definition                    body: (_                        "{"                        (_)* @function.inside                        "}")) @function.around                 ; Arrow function in variable declaration - capture the full declaration                ([                    (lexical_declaration                        (variable_declarator                            value: (arrow_function                                body: (statement_block                                    "{"                                    (_)* @function.inside                                    "}"))))                    (variable_declaration                        (variable_declarator                            value: (arrow_function                                body: (statement_block                                    "{"                                    (_)* @function.inside                                    "}"))))                ]) @function.around                 ([                    (lexical_declaration                        (variable_declarator                            value: (arrow_function)))                    (variable_declaration                        (variable_declarator                            value: (arrow_function)))                ]) @function.around                 ; Catch-all for arrow functions in other contexts (callbacks, etc.)                ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))                "#})),            ..Default::default()        })        .expect("Could not parse queries");         Self::new(language, capabilities, cx).await    }     pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {        let language = Language::new(            LanguageConfig {                name: "HTML".into(),                matcher: (LanguageMatcher {                    path_suffixes: vec!["html".into()],                    ..Default::default()                })                .into(),                block_comment: Some(BlockCommentConfig {                    start: "<!--".into(),                    prefix: "".into(),                    end: "-->".into(),                    tab_size: 0,                }),                completion_query_characters: ['-'].into_iter().collect(),                ..Default::default()            },            Some(tree_sitter_html::LANGUAGE.into()),        )        .with_queries(LanguageQueries {            brackets: Some(Cow::from(indoc! {r#"                ("<" @open "/>" @close)                ("</" @open ">" @close)                ("<" @open ">" @close)                ("\"" @open "\"" @close)"#})),            ..Default::default()        })        .expect("Could not parse queries");        Self::new(language, Default::default(), cx).await    }     pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {        let context = Self::new(            Arc::into_inner(markdown_lang()).unwrap(),            Default::default(),            cx,        )        .await;         let language_registry = context.workspace.read_with(cx, |workspace, cx| {            workspace.project().read(cx).languages().clone()        });        language_registry.add(rust_lang());         context    }     /// Constructs lsp range using a marked string with '[', ']' range delimiters    #[track_caller]    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {        let ranges = self.ranges(marked_text);        self.to_lsp_range(MultiBufferOffset(ranges[0].start)..MultiBufferOffset(ranges[0].end))    }     #[expect(clippy::wrong_self_convention, reason = "This is test code")]    pub fn to_lsp_range(&mut self, range: Range<MultiBufferOffset>) -> lsp::Range {        use language::ToPointUtf16;        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));        let start_point = range.start.to_point(&snapshot.buffer_snapshot());        let end_point = range.end.to_point(&snapshot.buffer_snapshot());         self.editor(|editor, _, cx| {            let buffer = editor.buffer().read(cx);            let (start_buffer, start_offset) =                buffer.point_to_buffer_offset(start_point, cx).unwrap();            let start = point_to_lsp(start_offset.to_point_utf16(&start_buffer.read(cx)));            let (end_buffer, end_offset) = buffer.point_to_buffer_offset(end_point, cx).unwrap();            let end = point_to_lsp(end_offset.to_point_utf16(&end_buffer.read(cx)));            lsp::Range { start, end }        })    }     #[expect(clippy::wrong_self_convention, reason = "This is test code")]    pub fn to_lsp(&mut self, offset: MultiBufferOffset) -> lsp::Position {        use language::ToPointUtf16;         let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));        let point = offset.to_point(&snapshot.buffer_snapshot());         self.editor(|editor, _, cx| {            let buffer = editor.buffer().read(cx);            let (buffer, offset) = buffer.point_to_buffer_offset(point, cx).unwrap();            point_to_lsp(offset.to_point_utf16(&buffer.read(cx)))        })    }     pub fn update_workspace<F, T>(&mut self, update: F) -> T    where        F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,    {        self.workspace.update_in(&mut self.cx.cx, update)    }     pub fn set_request_handler<T, F, Fut>(        &self,        mut handler: F,    ) -> futures::channel::mpsc::UnboundedReceiver<()>    where        T: 'static + request::Request,        T::Params: 'static + Send,        F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut,        Fut: 'static + Future<Output = Result<T::Result>>,    {        let url = self.buffer_lsp_url.clone();        self.lsp.set_request_handler::<T, _, _>(move |params, cx| {            let url = url.clone();            handler(url, params, cx)        })    }     pub fn notify<T: notification::Notification>(&self, params: T::Params) {

Lines 1–500 of 527.