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 point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries,
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}
82impl EditorLspTestContext {
83 pub async fn new(
84 language: Language,
85 capabilities: lsp::ServerCapabilities,
86 cx: &mut gpui::TestAppContext,
87 ) -> EditorLspTestContext {
88 let app_state = cx.update(AppState::test);
89
90 cx.update(|cx| {
91 assets::Assets.load_test_fonts(cx);
92 language::init(cx);
93 crate::init(cx);
94 workspace::init(app_state.clone(), cx);
95 Project::init_settings(cx);
96 });
97
98 let file_name = format!(
99 "file.{}",
100 language
101 .path_suffixes()
102 .first()
103 .expect("language must have a path suffix for EditorLspTestContext")
104 );
105
106 let project = Project::test(app_state.fs.clone(), [], cx).await;
107
108 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
109 let mut fake_servers = language_registry.register_fake_lsp(
110 language.name(),
111 FakeLspAdapter {
112 capabilities,
113 ..Default::default()
114 },
115 );
116 language_registry.add(Arc::new(language));
117
118 let root = Self::root_path();
119
120 app_state
121 .fs
122 .as_fake()
123 .insert_tree(
124 root,
125 json!({
126 ".git": {},
127 "dir": {
128 file_name.clone(): ""
129 }
130 }),
131 )
132 .await;
133
134 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
135
136 let workspace = window.root(cx).unwrap();
137
138 let mut cx = VisualTestContext::from_window(*window.deref(), cx);
139 project
140 .update(&mut cx, |project, cx| {
141 project.find_or_create_worktree(root, true, cx)
142 })
143 .await
144 .unwrap();
145 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
146 .await;
147 let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
148 let item = workspace
149 .update_in(&mut cx, |workspace, window, cx| {
150 workspace.open_path(file, None, true, window, cx)
151 })
152 .await
153 .expect("Could not open test file");
154 let editor = cx.update(|_, cx| {
155 item.act_as::<Editor>(cx)
156 .expect("Opened test file wasn't an editor")
157 });
158 editor.update_in(&mut cx, |editor, window, cx| {
159 window.focus(&editor.focus_handle(cx))
160 });
161
162 let lsp = fake_servers.next().await.unwrap();
163 Self {
164 cx: EditorTestContext {
165 cx,
166 window: window.into(),
167 editor,
168 assertion_cx: AssertionContextManager::new(),
169 },
170 lsp,
171 workspace,
172 buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
173 }
174 }
175
176 pub async fn new_rust(
177 capabilities: lsp::ServerCapabilities,
178 cx: &mut gpui::TestAppContext,
179 ) -> EditorLspTestContext {
180 Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
181 }
182
183 pub async fn new_typescript(
184 capabilities: lsp::ServerCapabilities,
185 cx: &mut gpui::TestAppContext,
186 ) -> EditorLspTestContext {
187 let mut word_characters: HashSet<char> = Default::default();
188 word_characters.insert('$');
189 word_characters.insert('#');
190 let language = Language::new(
191 LanguageConfig {
192 name: "Typescript".into(),
193 matcher: LanguageMatcher {
194 path_suffixes: vec!["ts".to_string()],
195 ..Default::default()
196 },
197 brackets: language::BracketPairConfig {
198 pairs: vec![language::BracketPair {
199 start: "{".to_string(),
200 end: "}".to_string(),
201 close: true,
202 surround: true,
203 newline: true,
204 }],
205 disabled_scopes_by_bracket_ix: Default::default(),
206 },
207 word_characters,
208 ..Default::default()
209 },
210 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
211 )
212 .with_queries(LanguageQueries {
213 brackets: Some(Cow::from(indoc! {r#"
214 ("(" @open ")" @close)
215 ("[" @open "]" @close)
216 ("{" @open "}" @close)
217 ("<" @open ">" @close)
218 ("\"" @open "\"" @close)"#})),
219 indents: Some(Cow::from(indoc! {r#"
220 [
221 (call_expression)
222 (assignment_expression)
223 (member_expression)
224 (lexical_declaration)
225 (variable_declaration)
226 (assignment_expression)
227 (if_statement)
228 (for_statement)
229 ] @indent
230
231 (_ "[" "]" @end) @indent
232 (_ "<" ">" @end) @indent
233 (_ "{" "}" @end) @indent
234 (_ "(" ")" @end) @indent
235 "#})),
236 ..Default::default()
237 })
238 .expect("Could not parse queries");
239
240 Self::new(language, capabilities, cx).await
241 }
242
243 pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
244 let language = Language::new(
245 LanguageConfig {
246 name: "HTML".into(),
247 matcher: LanguageMatcher {
248 path_suffixes: vec!["html".into()],
249 ..Default::default()
250 },
251 block_comment: Some(("<!-- ".into(), " -->".into())),
252 word_characters: ['-'].into_iter().collect(),
253 ..Default::default()
254 },
255 Some(tree_sitter_html::language()),
256 )
257 .with_queries(LanguageQueries {
258 brackets: Some(Cow::from(indoc! {r#"
259 ("<" @open "/>" @close)
260 ("</" @open ">" @close)
261 ("<" @open ">" @close)
262 ("\"" @open "\"" @close)"#})),
263 ..Default::default()
264 })
265 .expect("Could not parse queries");
266 Self::new(language, Default::default(), cx).await
267 }
268
269 /// Constructs lsp range using a marked string with '[', ']' range delimiters
270 #[track_caller]
271 pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
272 let ranges = self.ranges(marked_text);
273 self.to_lsp_range(ranges[0].clone())
274 }
275
276 pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
277 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
278 let start_point = range.start.to_point(&snapshot.buffer_snapshot);
279 let end_point = range.end.to_point(&snapshot.buffer_snapshot);
280
281 self.editor(|editor, _, cx| {
282 let buffer = editor.buffer().read(cx);
283 let start = point_to_lsp(
284 buffer
285 .point_to_buffer_offset(start_point, cx)
286 .unwrap()
287 .1
288 .to_point_utf16(&buffer.read(cx)),
289 );
290 let end = point_to_lsp(
291 buffer
292 .point_to_buffer_offset(end_point, cx)
293 .unwrap()
294 .1
295 .to_point_utf16(&buffer.read(cx)),
296 );
297
298 lsp::Range { start, end }
299 })
300 }
301
302 pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
303 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
304 let point = offset.to_point(&snapshot.buffer_snapshot);
305
306 self.editor(|editor, _, cx| {
307 let buffer = editor.buffer().read(cx);
308 point_to_lsp(
309 buffer
310 .point_to_buffer_offset(point, cx)
311 .unwrap()
312 .1
313 .to_point_utf16(&buffer.read(cx)),
314 )
315 })
316 }
317
318 pub fn update_workspace<F, T>(&mut self, update: F) -> T
319 where
320 F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
321 {
322 self.workspace.update_in(&mut self.cx.cx, update)
323 }
324
325 pub fn handle_request<T, F, Fut>(
326 &self,
327 mut handler: F,
328 ) -> futures::channel::mpsc::UnboundedReceiver<()>
329 where
330 T: 'static + request::Request,
331 T::Params: 'static + Send,
332 F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
333 Fut: 'static + Send + Future<Output = Result<T::Result>>,
334 {
335 let url = self.buffer_lsp_url.clone();
336 self.lsp.handle_request::<T, _, _>(move |params, cx| {
337 let url = url.clone();
338 handler(url, params, cx)
339 })
340 }
341
342 pub fn notify<T: notification::Notification>(&self, params: T::Params) {
343 self.lsp.notify::<T>(¶ms);
344 }
345
346 #[cfg(target_os = "windows")]
347 fn root_path() -> &'static Path {
348 Path::new("C:\\root")
349 }
350
351 #[cfg(not(target_os = "windows"))]
352 fn root_path() -> &'static Path {
353 Path::new("/root")
354 }
355}
356
357impl Deref for EditorLspTestContext {
358 type Target = EditorTestContext;
359
360 fn deref(&self) -> &Self::Target {
361 &self.cx
362 }
363}
364
365impl DerefMut for EditorLspTestContext {
366 fn deref_mut(&mut self) -> &mut Self::Target {
367 &mut self.cx
368 }
369}