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::{Context, Entity, Focusable as _, VisualTestContext, Window};
 15use indoc::indoc;
 16use language::{
 17    FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp,
 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: Entity<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        text_objects: Some(Cow::from(indoc! {r#"
 71            (function_item
 72                body: (_
 73                    "{"
 74                    (_)* @function.inside
 75                    "}" )) @function.around
 76        "#})),
 77        ..Default::default()
 78    })
 79    .expect("Could not parse queries");
 80    Arc::new(language)
 81}
 82
 83#[cfg(test)]
 84pub(crate) fn git_commit_lang() -> Arc<Language> {
 85    Arc::new(Language::new(
 86        LanguageConfig {
 87            name: "Git Commit".into(),
 88            line_comments: vec!["#".into()],
 89            ..Default::default()
 90        },
 91        None,
 92    ))
 93}
 94
 95impl EditorLspTestContext {
 96    pub async fn new(
 97        language: Language,
 98        capabilities: lsp::ServerCapabilities,
 99        cx: &mut gpui::TestAppContext,
100    ) -> EditorLspTestContext {
101        let app_state = cx.update(AppState::test);
102
103        cx.update(|cx| {
104            assets::Assets.load_test_fonts(cx);
105            language::init(cx);
106            crate::init(cx);
107            workspace::init(app_state.clone(), cx);
108            Project::init_settings(cx);
109        });
110
111        let file_name = format!(
112            "file.{}",
113            language
114                .path_suffixes()
115                .first()
116                .expect("language must have a path suffix for EditorLspTestContext")
117        );
118
119        let project = Project::test(app_state.fs.clone(), [], cx).await;
120
121        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
122        let mut fake_servers = language_registry.register_fake_lsp(
123            language.name(),
124            FakeLspAdapter {
125                capabilities,
126                ..Default::default()
127            },
128        );
129        language_registry.add(Arc::new(language));
130
131        let root = Self::root_path();
132
133        app_state
134            .fs
135            .as_fake()
136            .insert_tree(
137                root,
138                json!({
139                    ".git": {},
140                    "dir": {
141                        file_name.clone(): ""
142                    }
143                }),
144            )
145            .await;
146
147        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
148
149        let workspace = window.root(cx).unwrap();
150
151        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
152        project
153            .update(&mut cx, |project, cx| {
154                project.find_or_create_worktree(root, true, cx)
155            })
156            .await
157            .unwrap();
158        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
159            .await;
160        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
161        let item = workspace
162            .update_in(&mut cx, |workspace, window, cx| {
163                workspace.open_path(file, None, true, window, cx)
164            })
165            .await
166            .expect("Could not open test file");
167        let editor = cx.update(|_, cx| {
168            item.act_as::<Editor>(cx)
169                .expect("Opened test file wasn't an editor")
170        });
171        editor.update_in(&mut cx, |editor, window, cx| {
172            let nav_history = workspace
173                .read(cx)
174                .active_pane()
175                .read(cx)
176                .nav_history_for_item(&cx.entity());
177            editor.set_nav_history(Some(nav_history));
178            window.focus(&editor.focus_handle(cx))
179        });
180
181        let lsp = fake_servers.next().await.unwrap();
182        Self {
183            cx: EditorTestContext {
184                cx,
185                window: window.into(),
186                editor,
187                assertion_cx: AssertionContextManager::new(),
188            },
189            lsp,
190            workspace,
191            buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
192        }
193    }
194
195    pub async fn new_rust(
196        capabilities: lsp::ServerCapabilities,
197        cx: &mut gpui::TestAppContext,
198    ) -> EditorLspTestContext {
199        Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
200    }
201
202    pub async fn new_typescript(
203        capabilities: lsp::ServerCapabilities,
204        cx: &mut gpui::TestAppContext,
205    ) -> EditorLspTestContext {
206        let mut word_characters: HashSet<char> = Default::default();
207        word_characters.insert('$');
208        word_characters.insert('#');
209        let language = Language::new(
210            LanguageConfig {
211                name: "Typescript".into(),
212                matcher: LanguageMatcher {
213                    path_suffixes: vec!["ts".to_string()],
214                    ..Default::default()
215                },
216                brackets: language::BracketPairConfig {
217                    pairs: vec![language::BracketPair {
218                        start: "{".to_string(),
219                        end: "}".to_string(),
220                        close: true,
221                        surround: true,
222                        newline: true,
223                    }],
224                    disabled_scopes_by_bracket_ix: Default::default(),
225                },
226                word_characters,
227                ..Default::default()
228            },
229            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
230        )
231        .with_queries(LanguageQueries {
232            brackets: Some(Cow::from(indoc! {r#"
233                ("(" @open ")" @close)
234                ("[" @open "]" @close)
235                ("{" @open "}" @close)
236                ("<" @open ">" @close)
237                ("'" @open "'" @close)
238                ("`" @open "`" @close)
239                ("\"" @open "\"" @close)"#})),
240            indents: Some(Cow::from(indoc! {r#"
241                [
242                    (call_expression)
243                    (assignment_expression)
244                    (member_expression)
245                    (lexical_declaration)
246                    (variable_declaration)
247                    (assignment_expression)
248                    (if_statement)
249                    (for_statement)
250                ] @indent
251
252                (_ "[" "]" @end) @indent
253                (_ "<" ">" @end) @indent
254                (_ "{" "}" @end) @indent
255                (_ "(" ")" @end) @indent
256                "#})),
257            ..Default::default()
258        })
259        .expect("Could not parse queries");
260
261        Self::new(language, capabilities, cx).await
262    }
263
264    pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
265        let language = Language::new(
266            LanguageConfig {
267                name: "HTML".into(),
268                matcher: LanguageMatcher {
269                    path_suffixes: vec!["html".into()],
270                    ..Default::default()
271                },
272                block_comment: Some(("<!-- ".into(), " -->".into())),
273                completion_query_characters: ['-'].into_iter().collect(),
274                ..Default::default()
275            },
276            Some(tree_sitter_html::LANGUAGE.into()),
277        )
278        .with_queries(LanguageQueries {
279            brackets: Some(Cow::from(indoc! {r#"
280                ("<" @open "/>" @close)
281                ("</" @open ">" @close)
282                ("<" @open ">" @close)
283                ("\"" @open "\"" @close)"#})),
284            ..Default::default()
285        })
286        .expect("Could not parse queries");
287        Self::new(language, Default::default(), cx).await
288    }
289
290    /// Constructs lsp range using a marked string with '[', ']' range delimiters
291    #[track_caller]
292    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
293        let ranges = self.ranges(marked_text);
294        self.to_lsp_range(ranges[0].clone())
295    }
296
297    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
298        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
299        let start_point = range.start.to_point(&snapshot.buffer_snapshot);
300        let end_point = range.end.to_point(&snapshot.buffer_snapshot);
301
302        self.editor(|editor, _, cx| {
303            let buffer = editor.buffer().read(cx);
304            let start = point_to_lsp(
305                buffer
306                    .point_to_buffer_offset(start_point, cx)
307                    .unwrap()
308                    .1
309                    .to_point_utf16(&buffer.read(cx)),
310            );
311            let end = point_to_lsp(
312                buffer
313                    .point_to_buffer_offset(end_point, cx)
314                    .unwrap()
315                    .1
316                    .to_point_utf16(&buffer.read(cx)),
317            );
318
319            lsp::Range { start, end }
320        })
321    }
322
323    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
324        let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
325        let point = offset.to_point(&snapshot.buffer_snapshot);
326
327        self.editor(|editor, _, cx| {
328            let buffer = editor.buffer().read(cx);
329            point_to_lsp(
330                buffer
331                    .point_to_buffer_offset(point, cx)
332                    .unwrap()
333                    .1
334                    .to_point_utf16(&buffer.read(cx)),
335            )
336        })
337    }
338
339    pub fn update_workspace<F, T>(&mut self, update: F) -> T
340    where
341        F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
342    {
343        self.workspace.update_in(&mut self.cx.cx, update)
344    }
345
346    pub fn set_request_handler<T, F, Fut>(
347        &self,
348        mut handler: F,
349    ) -> futures::channel::mpsc::UnboundedReceiver<()>
350    where
351        T: 'static + request::Request,
352        T::Params: 'static + Send,
353        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
354        Fut: 'static + Future<Output = Result<T::Result>>,
355    {
356        let url = self.buffer_lsp_url.clone();
357        self.lsp.set_request_handler::<T, _, _>(move |params, cx| {
358            let url = url.clone();
359            handler(url, params, cx)
360        })
361    }
362
363    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
364        self.lsp.notify::<T>(&params);
365    }
366
367    #[cfg(target_os = "windows")]
368    fn root_path() -> &'static Path {
369        Path::new("C:\\root")
370    }
371
372    #[cfg(not(target_os = "windows"))]
373    fn root_path() -> &'static Path {
374        Path::new("/root")
375    }
376}
377
378impl Deref for EditorLspTestContext {
379    type Target = EditorTestContext;
380
381    fn deref(&self) -> &Self::Target {
382        &self.cx
383    }
384}
385
386impl DerefMut for EditorLspTestContext {
387    fn deref_mut(&mut self) -> &mut Self::Target {
388        &mut self.cx
389    }
390}