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 ("`" @open "`" @close)
220 ("\"" @open "\"" @close)"#})),
221 indents: Some(Cow::from(indoc! {r#"
222 [
223 (call_expression)
224 (assignment_expression)
225 (member_expression)
226 (lexical_declaration)
227 (variable_declaration)
228 (assignment_expression)
229 (if_statement)
230 (for_statement)
231 ] @indent
232
233 (_ "[" "]" @end) @indent
234 (_ "<" ">" @end) @indent
235 (_ "{" "}" @end) @indent
236 (_ "(" ")" @end) @indent
237 "#})),
238 ..Default::default()
239 })
240 .expect("Could not parse queries");
241
242 Self::new(language, capabilities, cx).await
243 }
244
245 pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
246 let language = Language::new(
247 LanguageConfig {
248 name: "HTML".into(),
249 matcher: LanguageMatcher {
250 path_suffixes: vec!["html".into()],
251 ..Default::default()
252 },
253 block_comment: Some(("<!-- ".into(), " -->".into())),
254 word_characters: ['-'].into_iter().collect(),
255 ..Default::default()
256 },
257 Some(tree_sitter_html::LANGUAGE.into()),
258 )
259 .with_queries(LanguageQueries {
260 brackets: Some(Cow::from(indoc! {r#"
261 ("<" @open "/>" @close)
262 ("</" @open ">" @close)
263 ("<" @open ">" @close)
264 ("\"" @open "\"" @close)"#})),
265 ..Default::default()
266 })
267 .expect("Could not parse queries");
268 Self::new(language, Default::default(), cx).await
269 }
270
271 /// Constructs lsp range using a marked string with '[', ']' range delimiters
272 #[track_caller]
273 pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
274 let ranges = self.ranges(marked_text);
275 self.to_lsp_range(ranges[0].clone())
276 }
277
278 pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
279 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
280 let start_point = range.start.to_point(&snapshot.buffer_snapshot);
281 let end_point = range.end.to_point(&snapshot.buffer_snapshot);
282
283 self.editor(|editor, _, cx| {
284 let buffer = editor.buffer().read(cx);
285 let start = point_to_lsp(
286 buffer
287 .point_to_buffer_offset(start_point, cx)
288 .unwrap()
289 .1
290 .to_point_utf16(&buffer.read(cx)),
291 );
292 let end = point_to_lsp(
293 buffer
294 .point_to_buffer_offset(end_point, cx)
295 .unwrap()
296 .1
297 .to_point_utf16(&buffer.read(cx)),
298 );
299
300 lsp::Range { start, end }
301 })
302 }
303
304 pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
305 let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
306 let point = offset.to_point(&snapshot.buffer_snapshot);
307
308 self.editor(|editor, _, cx| {
309 let buffer = editor.buffer().read(cx);
310 point_to_lsp(
311 buffer
312 .point_to_buffer_offset(point, cx)
313 .unwrap()
314 .1
315 .to_point_utf16(&buffer.read(cx)),
316 )
317 })
318 }
319
320 pub fn update_workspace<F, T>(&mut self, update: F) -> T
321 where
322 F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
323 {
324 self.workspace.update_in(&mut self.cx.cx, update)
325 }
326
327 pub fn handle_request<T, F, Fut>(
328 &self,
329 mut handler: F,
330 ) -> futures::channel::mpsc::UnboundedReceiver<()>
331 where
332 T: 'static + request::Request,
333 T::Params: 'static + Send,
334 F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
335 Fut: 'static + Send + Future<Output = Result<T::Result>>,
336 {
337 let url = self.buffer_lsp_url.clone();
338 self.lsp.handle_request::<T, _, _>(move |params, cx| {
339 let url = url.clone();
340 handler(url, params, cx)
341 })
342 }
343
344 pub fn notify<T: notification::Notification>(&self, params: T::Params) {
345 self.lsp.notify::<T>(¶ms);
346 }
347
348 #[cfg(target_os = "windows")]
349 fn root_path() -> &'static Path {
350 Path::new("C:\\root")
351 }
352
353 #[cfg(not(target_os = "windows"))]
354 fn root_path() -> &'static Path {
355 Path::new("/root")
356 }
357}
358
359impl Deref for EditorLspTestContext {
360 type Target = EditorTestContext;
361
362 fn deref(&self) -> &Self::Target {
363 &self.cx
364 }
365}
366
367impl DerefMut for EditorLspTestContext {
368 fn deref_mut(&mut self) -> &mut Self::Target {
369 &mut self.cx
370 }
371}