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