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