1use crate::{
2 LocationLink,
3 lsp_command::{
4 LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
5 },
6 lsp_store::LspStore,
7 make_text_document_identifier,
8};
9use anyhow::{Context as _, Result};
10use async_trait::async_trait;
11use collections::HashMap;
12use gpui::{App, AsyncApp, Entity};
13use language::{
14 Buffer, point_to_lsp,
15 proto::{deserialize_anchor, serialize_anchor},
16};
17use lsp::{LanguageServer, LanguageServerId};
18use rpc::proto::{self, PeerId};
19use serde::{Deserialize, Serialize};
20use std::{
21 path::{Path, PathBuf},
22 sync::Arc,
23};
24use task::TaskTemplate;
25use text::{BufferId, PointUtf16, ToPointUtf16};
26
27pub enum LspExpandMacro {}
28
29impl lsp::request::Request for LspExpandMacro {
30 type Params = ExpandMacroParams;
31 type Result = Option<ExpandedMacro>;
32 const METHOD: &'static str = "rust-analyzer/expandMacro";
33}
34
35#[derive(Deserialize, Serialize, Debug)]
36#[serde(rename_all = "camelCase")]
37pub struct ExpandMacroParams {
38 pub text_document: lsp::TextDocumentIdentifier,
39 pub position: lsp::Position,
40}
41
42#[derive(Default, Deserialize, Serialize, Debug)]
43#[serde(rename_all = "camelCase")]
44pub struct ExpandedMacro {
45 pub name: String,
46 pub expansion: String,
47}
48
49impl ExpandedMacro {
50 pub fn is_empty(&self) -> bool {
51 self.name.is_empty() && self.expansion.is_empty()
52 }
53}
54#[derive(Debug)]
55pub struct ExpandMacro {
56 pub position: PointUtf16,
57}
58
59#[async_trait(?Send)]
60impl LspCommand for ExpandMacro {
61 type Response = ExpandedMacro;
62 type LspRequest = LspExpandMacro;
63 type ProtoRequest = proto::LspExtExpandMacro;
64
65 fn display_name(&self) -> &str {
66 "Expand macro"
67 }
68
69 fn to_lsp(
70 &self,
71 path: &Path,
72 _: &Buffer,
73 _: &Arc<LanguageServer>,
74 _: &App,
75 ) -> Result<ExpandMacroParams> {
76 Ok(ExpandMacroParams {
77 text_document: make_text_document_identifier(path)?,
78 position: point_to_lsp(self.position),
79 })
80 }
81
82 async fn response_from_lsp(
83 self,
84 message: Option<ExpandedMacro>,
85 _: Entity<LspStore>,
86 _: Entity<Buffer>,
87 _: LanguageServerId,
88 _: AsyncApp,
89 ) -> anyhow::Result<ExpandedMacro> {
90 Ok(message
91 .map(|message| ExpandedMacro {
92 name: message.name,
93 expansion: message.expansion,
94 })
95 .unwrap_or_default())
96 }
97
98 fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
99 proto::LspExtExpandMacro {
100 project_id,
101 buffer_id: buffer.remote_id().into(),
102 position: Some(language::proto::serialize_anchor(
103 &buffer.anchor_before(self.position),
104 )),
105 }
106 }
107
108 async fn from_proto(
109 message: Self::ProtoRequest,
110 _: Entity<LspStore>,
111 buffer: Entity<Buffer>,
112 mut cx: AsyncApp,
113 ) -> anyhow::Result<Self> {
114 let position = message
115 .position
116 .and_then(deserialize_anchor)
117 .context("invalid position")?;
118 Ok(Self {
119 position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
120 })
121 }
122
123 fn response_to_proto(
124 response: ExpandedMacro,
125 _: &mut LspStore,
126 _: PeerId,
127 _: &clock::Global,
128 _: &mut App,
129 ) -> proto::LspExtExpandMacroResponse {
130 proto::LspExtExpandMacroResponse {
131 name: response.name,
132 expansion: response.expansion,
133 }
134 }
135
136 async fn response_from_proto(
137 self,
138 message: proto::LspExtExpandMacroResponse,
139 _: Entity<LspStore>,
140 _: Entity<Buffer>,
141 _: AsyncApp,
142 ) -> anyhow::Result<ExpandedMacro> {
143 Ok(ExpandedMacro {
144 name: message.name,
145 expansion: message.expansion,
146 })
147 }
148
149 fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> Result<BufferId> {
150 BufferId::new(message.buffer_id)
151 }
152}
153
154pub enum LspOpenDocs {}
155
156impl lsp::request::Request for LspOpenDocs {
157 type Params = OpenDocsParams;
158 type Result = Option<DocsUrls>;
159 const METHOD: &'static str = "experimental/externalDocs";
160}
161
162#[derive(Serialize, Deserialize, Debug)]
163#[serde(rename_all = "camelCase")]
164pub struct OpenDocsParams {
165 pub text_document: lsp::TextDocumentIdentifier,
166 pub position: lsp::Position,
167}
168
169#[derive(Serialize, Deserialize, Debug, Default)]
170#[serde(rename_all = "camelCase")]
171pub struct DocsUrls {
172 pub web: Option<String>,
173 pub local: Option<String>,
174}
175
176impl DocsUrls {
177 pub fn is_empty(&self) -> bool {
178 self.web.is_none() && self.local.is_none()
179 }
180}
181
182#[derive(Debug)]
183pub struct OpenDocs {
184 pub position: PointUtf16,
185}
186
187#[async_trait(?Send)]
188impl LspCommand for OpenDocs {
189 type Response = DocsUrls;
190 type LspRequest = LspOpenDocs;
191 type ProtoRequest = proto::LspExtOpenDocs;
192
193 fn display_name(&self) -> &str {
194 "Open docs"
195 }
196
197 fn to_lsp(
198 &self,
199 path: &Path,
200 _: &Buffer,
201 _: &Arc<LanguageServer>,
202 _: &App,
203 ) -> Result<OpenDocsParams> {
204 Ok(OpenDocsParams {
205 text_document: lsp::TextDocumentIdentifier {
206 uri: lsp::Url::from_file_path(path).unwrap(),
207 },
208 position: point_to_lsp(self.position),
209 })
210 }
211
212 async fn response_from_lsp(
213 self,
214 message: Option<DocsUrls>,
215 _: Entity<LspStore>,
216 _: Entity<Buffer>,
217 _: LanguageServerId,
218 _: AsyncApp,
219 ) -> anyhow::Result<DocsUrls> {
220 Ok(message
221 .map(|message| DocsUrls {
222 web: message.web,
223 local: message.local,
224 })
225 .unwrap_or_default())
226 }
227
228 fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs {
229 proto::LspExtOpenDocs {
230 project_id,
231 buffer_id: buffer.remote_id().into(),
232 position: Some(language::proto::serialize_anchor(
233 &buffer.anchor_before(self.position),
234 )),
235 }
236 }
237
238 async fn from_proto(
239 message: Self::ProtoRequest,
240 _: Entity<LspStore>,
241 buffer: Entity<Buffer>,
242 mut cx: AsyncApp,
243 ) -> anyhow::Result<Self> {
244 let position = message
245 .position
246 .and_then(deserialize_anchor)
247 .context("invalid position")?;
248 Ok(Self {
249 position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
250 })
251 }
252
253 fn response_to_proto(
254 response: DocsUrls,
255 _: &mut LspStore,
256 _: PeerId,
257 _: &clock::Global,
258 _: &mut App,
259 ) -> proto::LspExtOpenDocsResponse {
260 proto::LspExtOpenDocsResponse {
261 web: response.web,
262 local: response.local,
263 }
264 }
265
266 async fn response_from_proto(
267 self,
268 message: proto::LspExtOpenDocsResponse,
269 _: Entity<LspStore>,
270 _: Entity<Buffer>,
271 _: AsyncApp,
272 ) -> anyhow::Result<DocsUrls> {
273 Ok(DocsUrls {
274 web: message.web,
275 local: message.local,
276 })
277 }
278
279 fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result<BufferId> {
280 BufferId::new(message.buffer_id)
281 }
282}
283
284pub enum LspSwitchSourceHeader {}
285
286impl lsp::request::Request for LspSwitchSourceHeader {
287 type Params = SwitchSourceHeaderParams;
288 type Result = Option<SwitchSourceHeaderResult>;
289 const METHOD: &'static str = "textDocument/switchSourceHeader";
290}
291
292#[derive(Serialize, Deserialize, Debug)]
293#[serde(rename_all = "camelCase")]
294pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier);
295
296#[derive(Serialize, Deserialize, Debug, Default)]
297#[serde(rename_all = "camelCase")]
298pub struct SwitchSourceHeaderResult(pub String);
299
300#[derive(Default, Deserialize, Serialize, Debug)]
301#[serde(rename_all = "camelCase")]
302pub struct SwitchSourceHeader;
303
304#[async_trait(?Send)]
305impl LspCommand for SwitchSourceHeader {
306 type Response = SwitchSourceHeaderResult;
307 type LspRequest = LspSwitchSourceHeader;
308 type ProtoRequest = proto::LspExtSwitchSourceHeader;
309
310 fn display_name(&self) -> &str {
311 "Switch source header"
312 }
313
314 fn to_lsp(
315 &self,
316 path: &Path,
317 _: &Buffer,
318 _: &Arc<LanguageServer>,
319 _: &App,
320 ) -> Result<SwitchSourceHeaderParams> {
321 Ok(SwitchSourceHeaderParams(make_text_document_identifier(
322 path,
323 )?))
324 }
325
326 async fn response_from_lsp(
327 self,
328 message: Option<SwitchSourceHeaderResult>,
329 _: Entity<LspStore>,
330 _: Entity<Buffer>,
331 _: LanguageServerId,
332 _: AsyncApp,
333 ) -> anyhow::Result<SwitchSourceHeaderResult> {
334 Ok(message
335 .map(|message| SwitchSourceHeaderResult(message.0))
336 .unwrap_or_default())
337 }
338
339 fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader {
340 proto::LspExtSwitchSourceHeader {
341 project_id,
342 buffer_id: buffer.remote_id().into(),
343 }
344 }
345
346 async fn from_proto(
347 _: Self::ProtoRequest,
348 _: Entity<LspStore>,
349 _: Entity<Buffer>,
350 _: AsyncApp,
351 ) -> anyhow::Result<Self> {
352 Ok(Self {})
353 }
354
355 fn response_to_proto(
356 response: SwitchSourceHeaderResult,
357 _: &mut LspStore,
358 _: PeerId,
359 _: &clock::Global,
360 _: &mut App,
361 ) -> proto::LspExtSwitchSourceHeaderResponse {
362 proto::LspExtSwitchSourceHeaderResponse {
363 target_file: response.0,
364 }
365 }
366
367 async fn response_from_proto(
368 self,
369 message: proto::LspExtSwitchSourceHeaderResponse,
370 _: Entity<LspStore>,
371 _: Entity<Buffer>,
372 _: AsyncApp,
373 ) -> anyhow::Result<SwitchSourceHeaderResult> {
374 Ok(SwitchSourceHeaderResult(message.target_file))
375 }
376
377 fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result<BufferId> {
378 BufferId::new(message.buffer_id)
379 }
380}
381
382// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
383// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
384pub enum Runnables {}
385
386impl lsp::request::Request for Runnables {
387 type Params = RunnablesParams;
388 type Result = Vec<Runnable>;
389 const METHOD: &'static str = "experimental/runnables";
390}
391
392#[derive(Serialize, Deserialize, Debug, Clone)]
393#[serde(rename_all = "camelCase")]
394pub struct RunnablesParams {
395 pub text_document: lsp::TextDocumentIdentifier,
396 pub position: Option<lsp::Position>,
397}
398
399#[derive(Deserialize, Serialize, Debug, Clone)]
400#[serde(rename_all = "camelCase")]
401pub struct Runnable {
402 pub label: String,
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub location: Option<lsp::LocationLink>,
405 pub kind: RunnableKind,
406 pub args: RunnableArgs,
407}
408
409#[derive(Deserialize, Serialize, Debug, Clone)]
410#[serde(rename_all = "camelCase")]
411#[serde(untagged)]
412pub enum RunnableArgs {
413 Cargo(CargoRunnableArgs),
414 Shell(ShellRunnableArgs),
415}
416
417#[derive(Serialize, Deserialize, Debug, Clone)]
418#[serde(rename_all = "lowercase")]
419pub enum RunnableKind {
420 Cargo,
421 Shell,
422}
423
424#[derive(Deserialize, Serialize, Debug, Clone)]
425#[serde(rename_all = "camelCase")]
426pub struct CargoRunnableArgs {
427 #[serde(skip_serializing_if = "HashMap::is_empty")]
428 pub environment: HashMap<String, String>,
429 pub cwd: PathBuf,
430 /// Command to be executed instead of cargo
431 pub override_cargo: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub workspace_root: Option<PathBuf>,
434 // command, --package and --lib stuff
435 pub cargo_args: Vec<String>,
436 // stuff after --
437 pub executable_args: Vec<String>,
438}
439
440#[derive(Deserialize, Serialize, Debug, Clone)]
441#[serde(rename_all = "camelCase")]
442pub struct ShellRunnableArgs {
443 #[serde(skip_serializing_if = "HashMap::is_empty")]
444 pub environment: HashMap<String, String>,
445 pub cwd: PathBuf,
446 pub program: String,
447 pub args: Vec<String>,
448}
449
450#[derive(Debug)]
451pub struct GetLspRunnables {
452 pub buffer_id: BufferId,
453 pub position: Option<text::Anchor>,
454}
455
456#[derive(Debug, Default)]
457pub struct LspRunnables {
458 pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
459}
460
461#[async_trait(?Send)]
462impl LspCommand for GetLspRunnables {
463 type Response = LspRunnables;
464 type LspRequest = Runnables;
465 type ProtoRequest = proto::LspExtRunnables;
466
467 fn display_name(&self) -> &str {
468 "LSP Runnables"
469 }
470
471 fn to_lsp(
472 &self,
473 path: &Path,
474 buffer: &Buffer,
475 _: &Arc<LanguageServer>,
476 _: &App,
477 ) -> Result<RunnablesParams> {
478 let url = match lsp::Url::from_file_path(path) {
479 Ok(url) => url,
480 Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
481 };
482 Ok(RunnablesParams {
483 text_document: lsp::TextDocumentIdentifier::new(url),
484 position: self
485 .position
486 .map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
487 })
488 }
489
490 async fn response_from_lsp(
491 self,
492 lsp_runnables: Vec<Runnable>,
493 lsp_store: Entity<LspStore>,
494 buffer: Entity<Buffer>,
495 server_id: LanguageServerId,
496 mut cx: AsyncApp,
497 ) -> Result<LspRunnables> {
498 let mut runnables = Vec::with_capacity(lsp_runnables.len());
499
500 for runnable in lsp_runnables {
501 let location = match runnable.location {
502 Some(location) => Some(
503 location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
504 .await?,
505 ),
506 None => None,
507 };
508 let mut task_template = TaskTemplate::default();
509 task_template.label = runnable.label;
510 match runnable.args {
511 RunnableArgs::Cargo(cargo) => {
512 match cargo.override_cargo {
513 Some(override_cargo) => {
514 let mut override_parts =
515 override_cargo.split(" ").map(|s| s.to_string());
516 task_template.command = override_parts
517 .next()
518 .unwrap_or_else(|| override_cargo.clone());
519 task_template.args.extend(override_parts);
520 }
521 None => task_template.command = "cargo".to_string(),
522 };
523 task_template.env = cargo.environment;
524 task_template.cwd = Some(
525 cargo
526 .workspace_root
527 .unwrap_or(cargo.cwd)
528 .to_string_lossy()
529 .to_string(),
530 );
531 task_template.args.extend(cargo.cargo_args);
532 if !cargo.executable_args.is_empty() {
533 task_template.args.push("--".to_string());
534 task_template.args.extend(
535 cargo
536 .executable_args
537 .into_iter()
538 // rust-analyzer's doctest data may be smth. like
539 // ```
540 // command: "cargo",
541 // args: [
542 // "test",
543 // "--doc",
544 // "--package",
545 // "cargo-output-parser",
546 // "--",
547 // "X<T>::new",
548 // "--show-output",
549 // ],
550 // ```
551 // and `X<T>::new` will cause troubles if not escaped properly, as later
552 // the task runs as `$SHELL -i -c "cargo test ..."`.
553 //
554 // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`.
555 // That bit is not auto-expanded when using single quotes.
556 // Escape extra cargo args unconditionally as those are unlikely to contain `~`.
557 .map(|extra_arg| format!("'{extra_arg}'")),
558 );
559 }
560 }
561 RunnableArgs::Shell(shell) => {
562 task_template.command = shell.program;
563 task_template.args = shell.args;
564 task_template.env = shell.environment;
565 task_template.cwd = Some(shell.cwd.to_string_lossy().to_string());
566 }
567 }
568
569 runnables.push((location, task_template));
570 }
571
572 Ok(LspRunnables { runnables })
573 }
574
575 fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
576 proto::LspExtRunnables {
577 project_id,
578 buffer_id: buffer.remote_id().to_proto(),
579 position: self.position.as_ref().map(serialize_anchor),
580 }
581 }
582
583 async fn from_proto(
584 message: proto::LspExtRunnables,
585 _: Entity<LspStore>,
586 _: Entity<Buffer>,
587 _: AsyncApp,
588 ) -> Result<Self> {
589 let buffer_id = Self::buffer_id_from_proto(&message)?;
590 let position = message.position.and_then(deserialize_anchor);
591 Ok(Self {
592 buffer_id,
593 position,
594 })
595 }
596
597 fn response_to_proto(
598 response: LspRunnables,
599 lsp_store: &mut LspStore,
600 peer_id: PeerId,
601 _: &clock::Global,
602 cx: &mut App,
603 ) -> proto::LspExtRunnablesResponse {
604 proto::LspExtRunnablesResponse {
605 runnables: response
606 .runnables
607 .into_iter()
608 .map(|(location, task_template)| proto::LspRunnable {
609 location: location
610 .map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
611 task_template: serde_json::to_vec(&task_template).unwrap(),
612 })
613 .collect(),
614 }
615 }
616
617 async fn response_from_proto(
618 self,
619 message: proto::LspExtRunnablesResponse,
620 lsp_store: Entity<LspStore>,
621 _: Entity<Buffer>,
622 mut cx: AsyncApp,
623 ) -> Result<LspRunnables> {
624 let mut runnables = LspRunnables {
625 runnables: Vec::new(),
626 };
627
628 for lsp_runnable in message.runnables {
629 let location = match lsp_runnable.location {
630 Some(location) => {
631 Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
632 }
633 None => None,
634 };
635 let task_template = serde_json::from_slice(&lsp_runnable.task_template)
636 .context("deserializing task template from proto")?;
637 runnables.runnables.push((location, task_template));
638 }
639
640 Ok(runnables)
641 }
642
643 fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
644 BufferId::new(message.buffer_id)
645 }
646}