1use std::{
2 borrow::Cow,
3 ops::{Deref, DerefMut, Range},
4 path::Path,
5 sync::Arc,
6};
7
8use anyhow::Result;
9use serde_json::json;
10
11use crate::{Editor, ToPoint};
12use collections::HashSet;
13use futures::Future;
14use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window};
15use indoc::indoc;
16use language::{
17 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp,
18};
19use lsp::{notification, request};
20use multi_buffer::ToPointUtf16;
21use project::Project;
22use smol::stream::StreamExt;
23use workspace::{AppState, Workspace, WorkspaceHandle};
24
25use super::editor_test_context::{AssertionContextManager, EditorTestContext};
26
27pub struct EditorLspTestContext {
28 pub cx: EditorTestContext,
29 pub lsp: lsp::FakeLanguageServer,
30 pub workspace: Entity<Workspace>,
31 pub buffer_lsp_url: lsp::Url,
32}
33
34pub(crate) fn rust_lang() -> Arc<Language> {
35 let language = Language::new(
36 LanguageConfig {
37 name: "Rust".into(),
38 matcher: LanguageMatcher {
39 path_suffixes: vec!["rs".to_string()],
40 ..Default::default()
41 },
42 line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
43 ..Default::default()
44 },
45 Some(tree_sitter_rust::LANGUAGE.into()),
46 )
47 .with_queries(LanguageQueries {
48 indents: Some(Cow::from(indoc! {r#"
49 [
50 ((where_clause) _ @end)
51 (field_expression)
52 (call_expression)
53 (assignment_expression)
54 (let_declaration)
55 (let_chain)
56 (await_expression)
57 ] @indent
58
59 (_ "[" "]" @end) @indent
60 (_ "<" ">" @end) @indent
61 (_ "{" "}" @end) @indent
62 (_ "(" ")" @end) @indent"#})),
63 brackets: Some(Cow::from(indoc! {r#"
64 ("(" @open ")" @close)
65 ("[" @open "]" @close)
66 ("{" @open "}" @close)
67 ("<" @open ">" @close)
68 ("\"" @open "\"" @close)
69 (closure_parameters "|" @open "|" @close)"#})),
70 text_objects: Some(Cow::from(indoc! {r#"
71 (function_item
72 body: (_
73 "{"
74 (_)* @function.inside
75 "}" )) @function.around
76 "#})),
77 ..Default::default()
78 })
79 .expect("Could not parse queries");
80 Arc::new(language)
81}
82
83#[cfg(test)]
84pub(crate) fn git_commit_lang() -> Arc<Language> {
85 Arc::new(Language::new(
86 LanguageConfig {
87 name: "Git Commit".into(),
88 line_comments: vec!["#".into()],
89 ..Default::default()
90 },
91 None,
92 ))
93}
94
95impl EditorLspTestContext {
96 pub async fn new(
97 language: Language,
98 capabilities: lsp::ServerCapabilities,
99 cx: &mut gpui::TestAppContext,
100 ) -> EditorLspTestContext {
101 let app_state = cx.update(AppState::test);
102
103 cx.update(|cx| {
104 assets::Assets.load_test_fonts(cx);
105 language::init(cx);
106 crate::init(cx);
107 workspace::init(app_state.clone(), cx);
108 Project::init_settings(cx);
109 });
110
111 let file_name = format!(
112 "file.{}",
113 language
114 .path_suffixes()
115 .first()
116 .expect("language must have a path suffix for EditorLspTestContext")
117 );
118
119 let project = Project::test(app_state.fs.clone(), [], cx).await;
120
121 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
122 let mut fake_servers = language_registry.register_fake_lsp(
123 language.name(),
124 FakeLspAdapter {
125 capabilities,
126 ..Default::default()
127 },
128 );
129 language_registry.add(Arc::new(language));
130
131 let root = Self::root_path();
132
133 app_state
134 .fs
135 .as_fake()
136 .insert_tree(
137 root,
138 json!({
139 ".git": {},
140 "dir": {
141 file_name.clone(): ""
142 }
143 }),
144 )
145 .await;
146
147 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
148
149 let workspace = window.root(cx).unwrap();
150
151 let mut cx = VisualTestContext::from_window(*window.deref(), cx);
152 project
153 .update(&mut cx, |project, cx| {
154 project.find_or_create_worktree(root, true, cx)
155 })
156 .await
157 .unwrap();
158 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
159 .await;
160 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
161 let item = workspace
162 .update_in(&mut cx, |workspace, window, cx| {
163 workspace.open_path(file, None, true, window, cx)
164 })
165 .await
166 .expect("Could not open test file");
167 let editor = cx.update(|_, cx| {
168 item.act_as::<Editor>(cx)
169 .expect("Opened test file wasn't an editor")
170 });
171 editor.update_in(&mut cx, |editor, window, cx| {
172 window.focus(&editor.focus_handle(cx))
173 });
174
175 let lsp = fake_servers.next().await.unwrap();
176 Self {
177 cx: EditorTestContext {
178 cx,
179 window: window.into(),
180 editor,
181 assertion_cx: AssertionContextManager::new(),
182 },
183 lsp,
184 workspace,
185 buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
186 }
187 }
188
189 pub async fn new_rust(
190 capabilities: lsp::ServerCapabilities,
191 cx: &mut gpui::TestAppContext,
192 ) -> EditorLspTestContext {
193 Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
194 }
195
196 pub async fn new_typescript(
197 capabilities: lsp::ServerCapabilities,
198 cx: &mut gpui::TestAppContext,
199 ) -> EditorLspTestContext {
200 let mut word_characters: HashSet<char> = Default::default();
201 word_characters.insert('$');
202 word_characters.insert('#');
203 let language = Language::new(
204 LanguageConfig {
205 name: "Typescript".into(),
206 matcher: LanguageMatcher {
207 path_suffixes: vec!["ts".to_string()],
208 ..Default::default()
209 },
210 brackets: language::BracketPairConfig {
211 pairs: vec![language::BracketPair {
212 start: "{".to_string(),
213 end: "}".to_string(),
214 close: true,
215 surround: true,
216 newline: true,
217 }],
218 disabled_scopes_by_bracket_ix: Default::default(),
219 },
220 word_characters,
221 ..Default::default()
222 },
223 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
224 )
225 .with_queries(LanguageQueries {
226 brackets: Some(Cow::from(indoc! {r#"
227 ("(" @open ")" @close)
228 ("[" @open "]" @close)
229 ("{" @open "}" @close)
230 ("<" @open ">" @close)
231 ("'" @open "'" @close)
232 ("`" @open "`" @close)
233 ("\"" @open "\"" @close)"#})),
234 indents: Some(Cow::from(indoc! {r#"
235 [
236 (call_expression)
237 (assignment_expression)
238 (member_expression)
239 (lexical_declaration)
240 (variable_declaration)
241 (assignment_expression)
242 (if_statement)
243 (for_statement)
244 ] @indent
245
246 (_ "[" "]" @end) @indent
247 (_ "<" ">" @end) @indent
248 (_ "{" "}" @end) @indent
249 (_ "(" ")" @end) @indent
250 "#})),
251 ..Default::default()
252 })
253 .expect("Could not parse queries");
254
255 Self::new(language, capabilities, cx).await
256 }
257
258 pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
259 let language = Language::new(
260 LanguageConfig {
261 name: "HTML".into(),
262 matcher: LanguageMatcher {
263 path_suffixes: vec!["html".into()],
264 ..Default::default()
265 },
266 block_comment: Some(("<!-- ".into(), " -->".into())),
267 completion_query_characters: ['-'].into_iter().collect(),
268 ..Default::default()
269 },
270 Some(tree_sitter_html::LANGUAGE.into()),
271 )
272 .with_queries(LanguageQueries {
273 brackets: Some(Cow::from(indoc! {r#"
274 ("<" @open "/>" @close)
275 ("</" @open ">" @close)
276 ("<" @open ">" @close)
277 ("\"" @open "\"" @close)"#})),
278 ..Default::default()
279 })
280 .expect("Could not parse queries");
281 Self::new(language, Default::default(), cx).await
282 }
283
284 /// Constructs lsp range using a marked string with '[', ']' range delimiters
285 #[track_caller]
286 pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
287 let ranges = self.ranges(marked_text);
288 self.to_lsp_range(ranges[0].clone())
289 }
290
291 pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
292 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
293 let start_point = range.start.to_point(&snapshot.buffer_snapshot);
294 let end_point = range.end.to_point(&snapshot.buffer_snapshot);
295
296 self.editor(|editor, _, cx| {
297 let buffer = editor.buffer().read(cx);
298 let start = point_to_lsp(
299 buffer
300 .point_to_buffer_offset(start_point, cx)
301 .unwrap()
302 .1
303 .to_point_utf16(&buffer.read(cx)),
304 );
305 let end = point_to_lsp(
306 buffer
307 .point_to_buffer_offset(end_point, cx)
308 .unwrap()
309 .1
310 .to_point_utf16(&buffer.read(cx)),
311 );
312
313 lsp::Range { start, end }
314 })
315 }
316
317 pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
318 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
319 let point = offset.to_point(&snapshot.buffer_snapshot);
320
321 self.editor(|editor, _, cx| {
322 let buffer = editor.buffer().read(cx);
323 point_to_lsp(
324 buffer
325 .point_to_buffer_offset(point, cx)
326 .unwrap()
327 .1
328 .to_point_utf16(&buffer.read(cx)),
329 )
330 })
331 }
332
333 pub fn update_workspace<F, T>(&mut self, update: F) -> T
334 where
335 F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
336 {
337 self.workspace.update_in(&mut self.cx.cx, update)
338 }
339
340 pub fn set_request_handler<T, F, Fut>(
341 &self,
342 mut handler: F,
343 ) -> futures::channel::mpsc::UnboundedReceiver<()>
344 where
345 T: 'static + request::Request,
346 T::Params: 'static + Send,
347 F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
348 Fut: 'static + Send + Future<Output = Result<T::Result>>,
349 {
350 let url = self.buffer_lsp_url.clone();
351 self.lsp.set_request_handler::<T, _, _>(move |params, cx| {
352 let url = url.clone();
353 handler(url, params, cx)
354 })
355 }
356
357 pub fn notify<T: notification::Notification>(&self, params: T::Params) {
358 self.lsp.notify::<T>(¶ms);
359 }
360
361 #[cfg(target_os = "windows")]
362 fn root_path() -> &'static Path {
363 Path::new("C:\\root")
364 }
365
366 #[cfg(not(target_os = "windows"))]
367 fn root_path() -> &'static Path {
368 Path::new("/root")
369 }
370}
371
372impl Deref for EditorLspTestContext {
373 type Target = EditorTestContext;
374
375 fn deref(&self) -> &Self::Target {
376 &self.cx
377 }
378}
379
380impl DerefMut for EditorLspTestContext {
381 fn deref_mut(&mut self) -> &mut Self::Target {
382 &mut self.cx
383 }
384}