editor_lsp_test_context.rs

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