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