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