use std::{ borrow::Cow, ops::{Deref, DerefMut, Range}, path::Path, sync::Arc, }; use anyhow::Result; use serde_json::json; use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window}; use indoc::indoc; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp, }; use lsp::{notification, request}; use multi_buffer::ToPointUtf16; use project::Project; use smol::stream::StreamExt; use workspace::{AppState, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; pub struct EditorLspTestContext { pub cx: EditorTestContext, pub lsp: lsp::FakeLanguageServer, pub workspace: Entity, pub buffer_lsp_url: lsp::Url, } pub(crate) fn rust_lang() -> Arc { let language = Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ) .with_queries(LanguageQueries { indents: Some(Cow::from(indoc! {r#" [ ((where_clause) _ @end) (field_expression) (call_expression) (assignment_expression) (let_declaration) (let_chain) (await_expression) ] @indent (_ "[" "]" @end) @indent (_ "<" ">" @end) @indent (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent"#})), brackets: Some(Cow::from(indoc! {r#" ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close) (closure_parameters "|" @open "|" @close)"#})), text_objects: Some(Cow::from(indoc! {r#" (function_item body: (_ "{" (_)* @function.inside "}" )) @function.around "#})), ..Default::default() }) .expect("Could not parse queries"); Arc::new(language) } #[cfg(test)] pub(crate) fn git_commit_lang() -> Arc { 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); language::init(cx); crate::init(cx); workspace::init(app_state.clone(), cx); Project::init_settings(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| Workspace::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).worktree_scans_complete(cx)) .await; let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let item = workspace .update_in(&mut cx, |workspace, window, cx| { workspace.open_path(file, None, true, window, cx) }) .await .expect("Could not open test file"); let editor = cx.update(|_, cx| { item.act_as::(cx) .expect("Opened test file wasn't an editor") }); editor.update_in(&mut cx, |editor, window, cx| { window.focus(&editor.focus_handle(cx)) }); let lsp = fake_servers.next().await.unwrap(); Self { cx: EditorTestContext { cx, window: window.into(), editor, assertion_cx: AssertionContextManager::new(), }, lsp, workspace, buffer_lsp_url: lsp::Url::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 = 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() }, 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 "#})), ..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() }, block_comment: Some(("".into())), 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) ("" @close) ("<" @open ">" @close) ("\"" @open "\"" @close)"#})), ..Default::default() }) .expect("Could not parse queries"); Self::new(language, Default::default(), cx).await } /// 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(ranges[0].clone()) } pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { 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 = point_to_lsp( buffer .point_to_buffer_offset(start_point, cx) .unwrap() .1 .to_point_utf16(&buffer.read(cx)), ); let end = point_to_lsp( buffer .point_to_buffer_offset(end_point, cx) .unwrap() .1 .to_point_utf16(&buffer.read(cx)), ); lsp::Range { start, end } }) } pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { 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); point_to_lsp( buffer .point_to_buffer_offset(point, cx) .unwrap() .1 .to_point_utf16(&buffer.read(cx)), ) }) } pub fn update_workspace(&mut self, update: F) -> T where F: FnOnce(&mut Workspace, &mut Window, &mut Context) -> T, { self.workspace.update_in(&mut self.cx.cx, update) } pub fn set_request_handler( &self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, T::Params: 'static + Send, F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut, Fut: 'static + Send + Future>, { let url = self.buffer_lsp_url.clone(); self.lsp.set_request_handler::(move |params, cx| { let url = url.clone(); handler(url, params, cx) }) } pub fn notify(&self, params: T::Params) { self.lsp.notify::(¶ms); } #[cfg(target_os = "windows")] fn root_path() -> &'static Path { Path::new("C:\\root") } #[cfg(not(target_os = "windows"))] fn root_path() -> &'static Path { Path::new("/root") } } impl Deref for EditorLspTestContext { type Target = EditorTestContext; fn deref(&self) -> &Self::Target { &self.cx } } impl DerefMut for EditorLspTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } }