1use std::{
2 borrow::Cow,
3 ops::{Deref, DerefMut, Range},
4 sync::Arc,
5};
6
7use anyhow::Result;
8use client::Client;
9use serde_json::json;
10use util::http::FakeHttpClient;
11
12use crate::{Editor, ToPoint};
13use collections::HashSet;
14use futures::Future;
15use gpui::{View, ViewContext, VisualTestContext};
16use indoc::indoc;
17use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
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<'a> {
27 pub cx: EditorTestContext<'a>,
28 pub lsp: lsp::FakeLanguageServer,
29 pub workspace: View<Workspace>,
30 pub buffer_lsp_url: lsp::Url,
31}
32
33impl<'a> EditorLspTestContext<'a> {
34 pub async fn new(
35 mut language: Language,
36 capabilities: lsp::ServerCapabilities,
37 cx: &'a mut gpui::TestAppContext,
38 ) -> EditorLspTestContext<'a> {
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: &'a mut gpui::TestAppContext,
116 ) -> EditorLspTestContext<'a> {
117 let language = Language::new(
118 LanguageConfig {
119 name: "Rust".into(),
120 path_suffixes: vec!["rs".to_string()],
121 ..Default::default()
122 },
123 Some(tree_sitter_rust::language()),
124 )
125 .with_queries(LanguageQueries {
126 indents: Some(Cow::from(indoc! {r#"
127 [
128 ((where_clause) _ @end)
129 (field_expression)
130 (call_expression)
131 (assignment_expression)
132 (let_declaration)
133 (let_chain)
134 (await_expression)
135 ] @indent
136
137 (_ "[" "]" @end) @indent
138 (_ "<" ">" @end) @indent
139 (_ "{" "}" @end) @indent
140 (_ "(" ")" @end) @indent"#})),
141 brackets: Some(Cow::from(indoc! {r#"
142 ("(" @open ")" @close)
143 ("[" @open "]" @close)
144 ("{" @open "}" @close)
145 ("<" @open ">" @close)
146 ("\"" @open "\"" @close)
147 (closure_parameters "|" @open "|" @close)"#})),
148 ..Default::default()
149 })
150 .expect("Could not parse queries");
151
152 Self::new(language, capabilities, cx).await
153 }
154
155 pub async fn new_typescript(
156 capabilities: lsp::ServerCapabilities,
157 cx: &'a mut gpui::TestAppContext,
158 ) -> EditorLspTestContext<'a> {
159 let mut word_characters: HashSet<char> = Default::default();
160 word_characters.insert('$');
161 word_characters.insert('#');
162 let language = Language::new(
163 LanguageConfig {
164 name: "Typescript".into(),
165 path_suffixes: vec!["ts".to_string()],
166 brackets: language::BracketPairConfig {
167 pairs: vec![language::BracketPair {
168 start: "{".to_string(),
169 end: "}".to_string(),
170 close: true,
171 newline: true,
172 }],
173 disabled_scopes_by_bracket_ix: Default::default(),
174 },
175 word_characters,
176 ..Default::default()
177 },
178 Some(tree_sitter_typescript::language_typescript()),
179 )
180 .with_queries(LanguageQueries {
181 brackets: Some(Cow::from(indoc! {r#"
182 ("(" @open ")" @close)
183 ("[" @open "]" @close)
184 ("{" @open "}" @close)
185 ("<" @open ">" @close)
186 ("\"" @open "\"" @close)"#})),
187 indents: Some(Cow::from(indoc! {r#"
188 [
189 (call_expression)
190 (assignment_expression)
191 (member_expression)
192 (lexical_declaration)
193 (variable_declaration)
194 (assignment_expression)
195 (if_statement)
196 (for_statement)
197 ] @indent
198
199 (_ "[" "]" @end) @indent
200 (_ "<" ">" @end) @indent
201 (_ "{" "}" @end) @indent
202 (_ "(" ")" @end) @indent
203 "#})),
204 ..Default::default()
205 })
206 .expect("Could not parse queries");
207
208 Self::new(language, capabilities, cx).await
209 }
210
211 // Constructs lsp range using a marked string with '[', ']' range delimiters
212 pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
213 let ranges = self.ranges(marked_text);
214 self.to_lsp_range(ranges[0].clone())
215 }
216
217 pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
218 let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
219 let start_point = range.start.to_point(&snapshot.buffer_snapshot);
220 let end_point = range.end.to_point(&snapshot.buffer_snapshot);
221
222 self.editor(|editor, cx| {
223 let buffer = editor.buffer().read(cx);
224 let start = point_to_lsp(
225 buffer
226 .point_to_buffer_offset(start_point, cx)
227 .unwrap()
228 .1
229 .to_point_utf16(&buffer.read(cx)),
230 );
231 let end = point_to_lsp(
232 buffer
233 .point_to_buffer_offset(end_point, cx)
234 .unwrap()
235 .1
236 .to_point_utf16(&buffer.read(cx)),
237 );
238
239 lsp::Range { start, end }
240 })
241 }
242
243 pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
244 let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
245 let point = offset.to_point(&snapshot.buffer_snapshot);
246
247 self.editor(|editor, cx| {
248 let buffer = editor.buffer().read(cx);
249 point_to_lsp(
250 buffer
251 .point_to_buffer_offset(point, cx)
252 .unwrap()
253 .1
254 .to_point_utf16(&buffer.read(cx)),
255 )
256 })
257 }
258
259 pub fn update_workspace<F, T>(&mut self, update: F) -> T
260 where
261 F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
262 {
263 self.workspace.update(&mut self.cx.cx, update)
264 }
265
266 pub fn handle_request<T, F, Fut>(
267 &self,
268 mut handler: F,
269 ) -> futures::channel::mpsc::UnboundedReceiver<()>
270 where
271 T: 'static + request::Request,
272 T::Params: 'static + Send,
273 F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
274 Fut: 'static + Send + Future<Output = Result<T::Result>>,
275 {
276 let url = self.buffer_lsp_url.clone();
277 self.lsp.handle_request::<T, _, _>(move |params, cx| {
278 let url = url.clone();
279 handler(url, params, cx)
280 })
281 }
282
283 pub fn notify<T: notification::Notification>(&self, params: T::Params) {
284 self.lsp.notify::<T>(params);
285 }
286}
287
288impl<'a> Deref for EditorLspTestContext<'a> {
289 type Target = EditorTestContext<'a>;
290
291 fn deref(&self) -> &Self::Target {
292 &self.cx
293 }
294}
295
296impl<'a> DerefMut for EditorLspTestContext<'a> {
297 fn deref_mut(&mut self) -> &mut Self::Target {
298 &mut self.cx
299 }
300}