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