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