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