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