editor_lsp_test_context.rs

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