editor_lsp_test_context.rs

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