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