editor_lsp_test_context.rs

  1use std::{
  2    borrow::Cow,
  3    ops::{Deref, DerefMut, Range},
  4    path::Path,
  5    sync::Arc,
  6};
  7
  8use anyhow::Result;
  9use serde_json::json;
 10
 11use crate::{Editor, ToPoint};
 12use collections::HashSet;
 13use futures::Future;
 14use gpui::{View, ViewContext, VisualTestContext};
 15use indoc::indoc;
 16use language::{
 17    point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,
 18};
 19use lsp::{notification, request};
 20use multi_buffer::ToPointUtf16;
 21use project::Project;
 22use smol::stream::StreamExt;
 23use workspace::{AppState, Workspace, WorkspaceHandle};
 24
 25use super::editor_test_context::{AssertionContextManager, EditorTestContext};
 26
 27pub struct EditorLspTestContext {
 28    pub cx: EditorTestContext,
 29    pub lsp: lsp::FakeLanguageServer,
 30    pub workspace: View<Workspace>,
 31    pub buffer_lsp_url: lsp::Url,
 32}
 33
 34pub(crate) fn rust_lang() -> Arc<Language> {
 35    let language = Language::new(
 36        LanguageConfig {
 37            name: "Rust".into(),
 38            matcher: LanguageMatcher {
 39                path_suffixes: vec!["rs".to_string()],
 40                ..Default::default()
 41            },
 42            line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
 43            ..Default::default()
 44        },
 45        Some(tree_sitter_rust::LANGUAGE.into()),
 46    )
 47    .with_queries(LanguageQueries {
 48        indents: Some(Cow::from(indoc! {r#"
 49            [
 50                ((where_clause) _ @end)
 51                (field_expression)
 52                (call_expression)
 53                (assignment_expression)
 54                (let_declaration)
 55                (let_chain)
 56                (await_expression)
 57            ] @indent
 58
 59            (_ "[" "]" @end) @indent
 60            (_ "<" ">" @end) @indent
 61            (_ "{" "}" @end) @indent
 62            (_ "(" ")" @end) @indent"#})),
 63        brackets: Some(Cow::from(indoc! {r#"
 64            ("(" @open ")" @close)
 65            ("[" @open "]" @close)
 66            ("{" @open "}" @close)
 67            ("<" @open ">" @close)
 68            ("\"" @open "\"" @close)
 69            (closure_parameters "|" @open "|" @close)"#})),
 70        ..Default::default()
 71    })
 72    .expect("Could not parse queries");
 73    Arc::new(language)
 74}
 75impl EditorLspTestContext {
 76    pub async fn new(
 77        language: Language,
 78        capabilities: lsp::ServerCapabilities,
 79        cx: &mut gpui::TestAppContext,
 80    ) -> EditorLspTestContext {
 81        let app_state = cx.update(AppState::test);
 82
 83        cx.update(|cx| {
 84            assets::Assets.load_test_fonts(cx);
 85            language::init(cx);
 86            crate::init(cx);
 87            workspace::init(app_state.clone(), cx);
 88            Project::init_settings(cx);
 89        });
 90
 91        let file_name = format!(
 92            "file.{}",
 93            language
 94                .path_suffixes()
 95                .first()
 96                .expect("language must have a path suffix for EditorLspTestContext")
 97        );
 98
 99        let project = Project::test(app_state.fs.clone(), [], cx).await;
100
101        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
102        let mut fake_servers = language_registry.register_fake_lsp(
103            language.name(),
104            FakeLspAdapter {
105                capabilities,
106                ..Default::default()
107            },
108        );
109        language_registry.add(Arc::new(language));
110
111        let root = Self::root_path();
112
113        app_state
114            .fs
115            .as_fake()
116            .insert_tree(root, json!({ "dir": { file_name.clone(): "" }}))
117            .await;
118
119        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
120
121        let workspace = window.root_view(cx).unwrap();
122
123        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
124        project
125            .update(&mut cx, |project, cx| {
126                project.find_or_create_worktree(root, true, cx)
127            })
128            .await
129            .unwrap();
130        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
131            .await;
132        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
133        let item = workspace
134            .update(&mut cx, |workspace, cx| {
135                workspace.open_path(file, None, true, cx)
136            })
137            .await
138            .expect("Could not open test file");
139        let editor = cx.update(|cx| {
140            item.act_as::<Editor>(cx)
141                .expect("Opened test file wasn't an editor")
142        });
143        editor.update(&mut cx, |editor, cx| editor.focus(cx));
144
145        let lsp = fake_servers.next().await.unwrap();
146        Self {
147            cx: EditorTestContext {
148                cx,
149                window: window.into(),
150                editor,
151                assertion_cx: AssertionContextManager::new(),
152            },
153            lsp,
154            workspace,
155            buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
156        }
157    }
158
159    pub async fn new_rust(
160        capabilities: lsp::ServerCapabilities,
161        cx: &mut gpui::TestAppContext,
162    ) -> EditorLspTestContext {
163        Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
164    }
165
166    pub async fn new_typescript(
167        capabilities: lsp::ServerCapabilities,
168        cx: &mut gpui::TestAppContext,
169    ) -> EditorLspTestContext {
170        let mut word_characters: HashSet<char> = Default::default();
171        word_characters.insert('$');
172        word_characters.insert('#');
173        let language = Language::new(
174            LanguageConfig {
175                name: "Typescript".into(),
176                matcher: LanguageMatcher {
177                    path_suffixes: vec!["ts".to_string()],
178                    ..Default::default()
179                },
180                brackets: language::BracketPairConfig {
181                    pairs: vec![language::BracketPair {
182                        start: "{".to_string(),
183                        end: "}".to_string(),
184                        close: true,
185                        surround: true,
186                        newline: true,
187                    }],
188                    disabled_scopes_by_bracket_ix: Default::default(),
189                },
190                word_characters,
191                ..Default::default()
192            },
193            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
194        )
195        .with_queries(LanguageQueries {
196            brackets: Some(Cow::from(indoc! {r#"
197                ("(" @open ")" @close)
198                ("[" @open "]" @close)
199                ("{" @open "}" @close)
200                ("<" @open ">" @close)
201                ("\"" @open "\"" @close)"#})),
202            indents: Some(Cow::from(indoc! {r#"
203                [
204                    (call_expression)
205                    (assignment_expression)
206                    (member_expression)
207                    (lexical_declaration)
208                    (variable_declaration)
209                    (assignment_expression)
210                    (if_statement)
211                    (for_statement)
212                ] @indent
213
214                (_ "[" "]" @end) @indent
215                (_ "<" ">" @end) @indent
216                (_ "{" "}" @end) @indent
217                (_ "(" ")" @end) @indent
218                "#})),
219            ..Default::default()
220        })
221        .expect("Could not parse queries");
222
223        Self::new(language, capabilities, cx).await
224    }
225
226    pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
227        let language = Language::new(
228            LanguageConfig {
229                name: "HTML".into(),
230                matcher: LanguageMatcher {
231                    path_suffixes: vec!["html".into()],
232                    ..Default::default()
233                },
234                block_comment: Some(("<!-- ".into(), " -->".into())),
235                word_characters: ['-'].into_iter().collect(),
236                ..Default::default()
237            },
238            Some(tree_sitter_html::language()),
239        )
240        .with_queries(LanguageQueries {
241            brackets: Some(Cow::from(indoc! {r#"
242                ("<" @open "/>" @close)
243                ("</" @open ">" @close)
244                ("<" @open ">" @close)
245                ("\"" @open "\"" @close)"#})),
246            ..Default::default()
247        })
248        .expect("Could not parse queries");
249        Self::new(language, Default::default(), cx).await
250    }
251
252    // Constructs lsp range using a marked string with '[', ']' range delimiters
253    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
254        let ranges = self.ranges(marked_text);
255        self.to_lsp_range(ranges[0].clone())
256    }
257
258    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
259        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
260        let start_point = range.start.to_point(&snapshot.buffer_snapshot);
261        let end_point = range.end.to_point(&snapshot.buffer_snapshot);
262
263        self.editor(|editor, cx| {
264            let buffer = editor.buffer().read(cx);
265            let start = point_to_lsp(
266                buffer
267                    .point_to_buffer_offset(start_point, cx)
268                    .unwrap()
269                    .1
270                    .to_point_utf16(&buffer.read(cx)),
271            );
272            let end = point_to_lsp(
273                buffer
274                    .point_to_buffer_offset(end_point, cx)
275                    .unwrap()
276                    .1
277                    .to_point_utf16(&buffer.read(cx)),
278            );
279
280            lsp::Range { start, end }
281        })
282    }
283
284    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
285        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
286        let point = offset.to_point(&snapshot.buffer_snapshot);
287
288        self.editor(|editor, cx| {
289            let buffer = editor.buffer().read(cx);
290            point_to_lsp(
291                buffer
292                    .point_to_buffer_offset(point, cx)
293                    .unwrap()
294                    .1
295                    .to_point_utf16(&buffer.read(cx)),
296            )
297        })
298    }
299
300    pub fn update_workspace<F, T>(&mut self, update: F) -> T
301    where
302        F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
303    {
304        self.workspace.update(&mut self.cx.cx, update)
305    }
306
307    pub fn handle_request<T, F, Fut>(
308        &self,
309        mut handler: F,
310    ) -> futures::channel::mpsc::UnboundedReceiver<()>
311    where
312        T: 'static + request::Request,
313        T::Params: 'static + Send,
314        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
315        Fut: 'static + Send + Future<Output = Result<T::Result>>,
316    {
317        let url = self.buffer_lsp_url.clone();
318        self.lsp.handle_request::<T, _, _>(move |params, cx| {
319            let url = url.clone();
320            handler(url, params, cx)
321        })
322    }
323
324    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
325        self.lsp.notify::<T>(params);
326    }
327
328    #[cfg(target_os = "windows")]
329    fn root_path() -> &'static Path {
330        Path::new("C:\\root")
331    }
332
333    #[cfg(not(target_os = "windows"))]
334    fn root_path() -> &'static Path {
335        Path::new("/root")
336    }
337}
338
339impl Deref for EditorLspTestContext {
340    type Target = EditorTestContext;
341
342    fn deref(&self) -> &Self::Target {
343        &self.cx
344    }
345}
346
347impl DerefMut for EditorLspTestContext {
348    fn deref_mut(&mut self) -> &mut Self::Target {
349        &mut self.cx
350    }
351}