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    #[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}