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 #[serde(default)]
397 pub position: Option<lsp::Position>,
398}
399
400#[derive(Deserialize, Serialize, Debug, Clone)]
401#[serde(rename_all = "camelCase")]
402pub struct Runnable {
403 pub label: String,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub location: Option<lsp::LocationLink>,
406 pub kind: RunnableKind,
407 pub args: RunnableArgs,
408}
409
410#[derive(Deserialize, Serialize, Debug, Clone)]
411#[serde(rename_all = "camelCase")]
412#[serde(untagged)]
413pub enum RunnableArgs {
414 Cargo(CargoRunnableArgs),
415 Shell(ShellRunnableArgs),
416}
417
418#[derive(Serialize, Deserialize, Debug, Clone)]
419#[serde(rename_all = "lowercase")]
420pub enum RunnableKind {
421 Cargo,
422 Shell,
423}
424
425#[derive(Deserialize, Serialize, Debug, Clone)]
426#[serde(rename_all = "camelCase")]
427pub struct CargoRunnableArgs {
428 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
429 pub environment: HashMap<String, String>,
430 pub cwd: PathBuf,
431 /// Command to be executed instead of cargo
432 #[serde(default)]
433 pub override_cargo: Option<String>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub workspace_root: Option<PathBuf>,
436 // command, --package and --lib stuff
437 #[serde(default)]
438 pub cargo_args: Vec<String>,
439 // stuff after --
440 #[serde(default)]
441 pub executable_args: Vec<String>,
442}
443
444#[derive(Deserialize, Serialize, Debug, Clone)]
445#[serde(rename_all = "camelCase")]
446pub struct ShellRunnableArgs {
447 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
448 pub environment: HashMap<String, String>,
449 pub cwd: PathBuf,
450 pub program: String,
451 #[serde(default)]
452 pub args: Vec<String>,
453}
454
455#[derive(Debug)]
456pub struct GetLspRunnables {
457 pub buffer_id: BufferId,
458 pub position: Option<text::Anchor>,
459}
460
461#[derive(Debug, Default)]
462pub struct LspRunnables {
463 pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
464}
465
466#[async_trait(?Send)]
467impl LspCommand for GetLspRunnables {
468 type Response = LspRunnables;
469 type LspRequest = Runnables;
470 type ProtoRequest = proto::LspExtRunnables;
471
472 fn display_name(&self) -> &str {
473 "LSP Runnables"
474 }
475
476 fn to_lsp(
477 &self,
478 path: &Path,
479 buffer: &Buffer,
480 _: &Arc<LanguageServer>,
481 _: &App,
482 ) -> Result<RunnablesParams> {
483 let url = match lsp::Url::from_file_path(path) {
484 Ok(url) => url,
485 Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
486 };
487 Ok(RunnablesParams {
488 text_document: lsp::TextDocumentIdentifier::new(url),
489 position: self
490 .position
491 .map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
492 })
493 }
494
495 async fn response_from_lsp(
496 self,
497 lsp_runnables: Vec<Runnable>,
498 lsp_store: Entity<LspStore>,
499 buffer: Entity<Buffer>,
500 server_id: LanguageServerId,
501 mut cx: AsyncApp,
502 ) -> Result<LspRunnables> {
503 let mut runnables = Vec::with_capacity(lsp_runnables.len());
504
505 for runnable in lsp_runnables {
506 let location = match runnable.location {
507 Some(location) => Some(
508 location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
509 .await?,
510 ),
511 None => None,
512 };
513 let mut task_template = TaskTemplate::default();
514 task_template.label = runnable.label;
515 match runnable.args {
516 RunnableArgs::Cargo(cargo) => {
517 match cargo.override_cargo {
518 Some(override_cargo) => {
519 let mut override_parts =
520 override_cargo.split(" ").map(|s| s.to_string());
521 task_template.command = override_parts
522 .next()
523 .unwrap_or_else(|| override_cargo.clone());
524 task_template.args.extend(override_parts);
525 }
526 None => task_template.command = "cargo".to_string(),
527 };
528 task_template.env = cargo.environment;
529 task_template.cwd = Some(
530 cargo
531 .workspace_root
532 .unwrap_or(cargo.cwd)
533 .to_string_lossy()
534 .to_string(),
535 );
536 task_template.args.extend(cargo.cargo_args);
537 if !cargo.executable_args.is_empty() {
538 task_template.args.push("--".to_string());
539 task_template.args.extend(
540 cargo
541 .executable_args
542 .into_iter()
543 // rust-analyzer's doctest data may be smth. like
544 // ```
545 // command: "cargo",
546 // args: [
547 // "test",
548 // "--doc",
549 // "--package",
550 // "cargo-output-parser",
551 // "--",
552 // "X<T>::new",
553 // "--show-output",
554 // ],
555 // ```
556 // and `X<T>::new` will cause troubles if not escaped properly, as later
557 // the task runs as `$SHELL -i -c "cargo test ..."`.
558 //
559 // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`.
560 // That bit is not auto-expanded when using single quotes.
561 // Escape extra cargo args unconditionally as those are unlikely to contain `~`.
562 .map(|extra_arg| format!("'{extra_arg}'")),
563 );
564 }
565 }
566 RunnableArgs::Shell(shell) => {
567 task_template.command = shell.program;
568 task_template.args = shell.args;
569 task_template.env = shell.environment;
570 task_template.cwd = Some(shell.cwd.to_string_lossy().to_string());
571 }
572 }
573
574 runnables.push((location, task_template));
575 }
576
577 Ok(LspRunnables { runnables })
578 }
579
580 fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
581 proto::LspExtRunnables {
582 project_id,
583 buffer_id: buffer.remote_id().to_proto(),
584 position: self.position.as_ref().map(serialize_anchor),
585 }
586 }
587
588 async fn from_proto(
589 message: proto::LspExtRunnables,
590 _: Entity<LspStore>,
591 _: Entity<Buffer>,
592 _: AsyncApp,
593 ) -> Result<Self> {
594 let buffer_id = Self::buffer_id_from_proto(&message)?;
595 let position = message.position.and_then(deserialize_anchor);
596 Ok(Self {
597 buffer_id,
598 position,
599 })
600 }
601
602 fn response_to_proto(
603 response: LspRunnables,
604 lsp_store: &mut LspStore,
605 peer_id: PeerId,
606 _: &clock::Global,
607 cx: &mut App,
608 ) -> proto::LspExtRunnablesResponse {
609 proto::LspExtRunnablesResponse {
610 runnables: response
611 .runnables
612 .into_iter()
613 .map(|(location, task_template)| proto::LspRunnable {
614 location: location
615 .map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
616 task_template: serde_json::to_vec(&task_template).unwrap(),
617 })
618 .collect(),
619 }
620 }
621
622 async fn response_from_proto(
623 self,
624 message: proto::LspExtRunnablesResponse,
625 lsp_store: Entity<LspStore>,
626 _: Entity<Buffer>,
627 mut cx: AsyncApp,
628 ) -> Result<LspRunnables> {
629 let mut runnables = LspRunnables {
630 runnables: Vec::new(),
631 };
632
633 for lsp_runnable in message.runnables {
634 let location = match lsp_runnable.location {
635 Some(location) => {
636 Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
637 }
638 None => None,
639 };
640 let task_template = serde_json::from_slice(&lsp_runnable.task_template)
641 .context("deserializing task template from proto")?;
642 runnables.runnables.push((location, task_template));
643 }
644
645 Ok(runnables)
646 }
647
648 fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
649 BufferId::new(message.buffer_id)
650 }
651}