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