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