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