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