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