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