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