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 let nav_history = workspace
173 .read(cx)
174 .active_pane()
175 .read(cx)
176 .nav_history_for_item(&cx.entity());
177 editor.set_nav_history(Some(nav_history));
178 window.focus(&editor.focus_handle(cx))
179 });
180
181 let lsp = fake_servers.next().await.unwrap();
182 Self {
183 cx: EditorTestContext {
184 cx,
185 window: window.into(),
186 editor,
187 assertion_cx: AssertionContextManager::new(),
188 },
189 lsp,
190 workspace,
191 buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
192 }
193 }
194
195 pub async fn new_rust(
196 capabilities: lsp::ServerCapabilities,
197 cx: &mut gpui::TestAppContext,
198 ) -> EditorLspTestContext {
199 Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
200 }
201
202 pub async fn new_typescript(
203 capabilities: lsp::ServerCapabilities,
204 cx: &mut gpui::TestAppContext,
205 ) -> EditorLspTestContext {
206 let mut word_characters: HashSet<char> = Default::default();
207 word_characters.insert('$');
208 word_characters.insert('#');
209 let language = Language::new(
210 LanguageConfig {
211 name: "Typescript".into(),
212 matcher: LanguageMatcher {
213 path_suffixes: vec!["ts".to_string()],
214 ..Default::default()
215 },
216 brackets: language::BracketPairConfig {
217 pairs: vec![language::BracketPair {
218 start: "{".to_string(),
219 end: "}".to_string(),
220 close: true,
221 surround: true,
222 newline: true,
223 }],
224 disabled_scopes_by_bracket_ix: Default::default(),
225 },
226 word_characters,
227 ..Default::default()
228 },
229 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
230 )
231 .with_queries(LanguageQueries {
232 brackets: Some(Cow::from(indoc! {r#"
233 ("(" @open ")" @close)
234 ("[" @open "]" @close)
235 ("{" @open "}" @close)
236 ("<" @open ">" @close)
237 ("'" @open "'" @close)
238 ("`" @open "`" @close)
239 ("\"" @open "\"" @close)"#})),
240 indents: Some(Cow::from(indoc! {r#"
241 [
242 (call_expression)
243 (assignment_expression)
244 (member_expression)
245 (lexical_declaration)
246 (variable_declaration)
247 (assignment_expression)
248 (if_statement)
249 (for_statement)
250 ] @indent
251
252 (_ "[" "]" @end) @indent
253 (_ "<" ">" @end) @indent
254 (_ "{" "}" @end) @indent
255 (_ "(" ")" @end) @indent
256 "#})),
257 ..Default::default()
258 })
259 .expect("Could not parse queries");
260
261 Self::new(language, capabilities, cx).await
262 }
263
264 pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
265 let language = Language::new(
266 LanguageConfig {
267 name: "HTML".into(),
268 matcher: LanguageMatcher {
269 path_suffixes: vec!["html".into()],
270 ..Default::default()
271 },
272 block_comment: Some(("<!-- ".into(), " -->".into())),
273 completion_query_characters: ['-'].into_iter().collect(),
274 ..Default::default()
275 },
276 Some(tree_sitter_html::LANGUAGE.into()),
277 )
278 .with_queries(LanguageQueries {
279 brackets: Some(Cow::from(indoc! {r#"
280 ("<" @open "/>" @close)
281 ("</" @open ">" @close)
282 ("<" @open ">" @close)
283 ("\"" @open "\"" @close)"#})),
284 ..Default::default()
285 })
286 .expect("Could not parse queries");
287 Self::new(language, Default::default(), cx).await
288 }
289
290 /// Constructs lsp range using a marked string with '[', ']' range delimiters
291 #[track_caller]
292 pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
293 let ranges = self.ranges(marked_text);
294 self.to_lsp_range(ranges[0].clone())
295 }
296
297 pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
298 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
299 let start_point = range.start.to_point(&snapshot.buffer_snapshot);
300 let end_point = range.end.to_point(&snapshot.buffer_snapshot);
301
302 self.editor(|editor, _, cx| {
303 let buffer = editor.buffer().read(cx);
304 let start = point_to_lsp(
305 buffer
306 .point_to_buffer_offset(start_point, cx)
307 .unwrap()
308 .1
309 .to_point_utf16(&buffer.read(cx)),
310 );
311 let end = point_to_lsp(
312 buffer
313 .point_to_buffer_offset(end_point, cx)
314 .unwrap()
315 .1
316 .to_point_utf16(&buffer.read(cx)),
317 );
318
319 lsp::Range { start, end }
320 })
321 }
322
323 pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
324 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
325 let point = offset.to_point(&snapshot.buffer_snapshot);
326
327 self.editor(|editor, _, cx| {
328 let buffer = editor.buffer().read(cx);
329 point_to_lsp(
330 buffer
331 .point_to_buffer_offset(point, cx)
332 .unwrap()
333 .1
334 .to_point_utf16(&buffer.read(cx)),
335 )
336 })
337 }
338
339 pub fn update_workspace<F, T>(&mut self, update: F) -> T
340 where
341 F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
342 {
343 self.workspace.update_in(&mut self.cx.cx, update)
344 }
345
346 pub fn set_request_handler<T, F, Fut>(
347 &self,
348 mut handler: F,
349 ) -> futures::channel::mpsc::UnboundedReceiver<()>
350 where
351 T: 'static + request::Request,
352 T::Params: 'static + Send,
353 F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
354 Fut: 'static + Future<Output = Result<T::Result>>,
355 {
356 let url = self.buffer_lsp_url.clone();
357 self.lsp.set_request_handler::<T, _, _>(move |params, cx| {
358 let url = url.clone();
359 handler(url, params, cx)
360 })
361 }
362
363 pub fn notify<T: notification::Notification>(&self, params: T::Params) {
364 self.lsp.notify::<T>(¶ms);
365 }
366
367 #[cfg(target_os = "windows")]
368 fn root_path() -> &'static Path {
369 Path::new("C:\\root")
370 }
371
372 #[cfg(not(target_os = "windows"))]
373 fn root_path() -> &'static Path {
374 Path::new("/root")
375 }
376}
377
378impl Deref for EditorLspTestContext {
379 type Target = EditorTestContext;
380
381 fn deref(&self) -> &Self::Target {
382 &self.cx
383 }
384}
385
386impl DerefMut for EditorLspTestContext {
387 fn deref_mut(&mut self) -> &mut Self::Target {
388 &mut self.cx
389 }
390}