lsp_ext_command.rs

  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}