Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:57:42.293Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

lsp_ext_command.rs

881 lines · 26.8 KB · rust
1use crate::{
2    LocationLink,
3    lsp_command::{
4        LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto,
5        location_link_to_proto, location_links_from_lsp, location_links_from_proto,
6        location_links_to_proto,
7    },
8    lsp_store::LspStore,
9    make_lsp_text_document_position, make_text_document_identifier,
10};
11use anyhow::{Context as _, Result};
12use async_trait::async_trait;
13use collections::HashMap;
14use gpui::{App, AsyncApp, Entity};
15use language::{
16    Buffer, point_to_lsp,
17    proto::{deserialize_anchor, serialize_anchor},
18};
19use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId};
20use rpc::proto::{self, PeerId};
21use serde::{Deserialize, Serialize};
22use std::{
23    path::{Path, PathBuf},
24    sync::Arc,
25};
26use task::TaskTemplate;
27use text::{BufferId, PointUtf16, ToPointUtf16};
28
29pub enum LspExtExpandMacro {}
30
31impl lsp::request::Request for LspExtExpandMacro {
32    type Params = ExpandMacroParams;
33    type Result = Option<ExpandedMacro>;
34    const METHOD: &'static str = "rust-analyzer/expandMacro";
35}
36
37#[derive(Deserialize, Serialize, Debug)]
38#[serde(rename_all = "camelCase")]
39pub struct ExpandMacroParams {
40    pub text_document: lsp::TextDocumentIdentifier,
41    pub position: lsp::Position,
42}
43
44#[derive(Default, Deserialize, Serialize, Debug)]
45#[serde(rename_all = "camelCase")]
46pub struct ExpandedMacro {
47    pub name: String,
48    pub expansion: String,
49}
50
51impl ExpandedMacro {
52    pub fn is_empty(&self) -> bool {
53        self.name.is_empty() && self.expansion.is_empty()
54    }
55}
56#[derive(Debug)]
57pub struct ExpandMacro {
58    pub position: PointUtf16,
59}
60
61#[async_trait(?Send)]
62impl LspCommand for ExpandMacro {
63    type Response = ExpandedMacro;
64    type LspRequest = LspExtExpandMacro;
65    type ProtoRequest = proto::LspExtExpandMacro;
66
67    fn display_name(&self) -> &str {
68        "Expand macro"
69    }
70
71    fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
72        true
73    }
74
75    fn to_lsp(
76        &self,
77        path: &Path,
78        _: &Buffer,
79        _: &Arc<LanguageServer>,
80        _: &App,
81    ) -> Result<ExpandMacroParams> {
82        Ok(ExpandMacroParams {
83            text_document: make_text_document_identifier(path)?,
84            position: point_to_lsp(self.position),
85        })
86    }
87
88    async fn response_from_lsp(
89        self,
90        message: Option<ExpandedMacro>,
91        _: Entity<LspStore>,
92        _: Entity<Buffer>,
93        _: LanguageServerId,
94        _: AsyncApp,
95    ) -> anyhow::Result<ExpandedMacro> {
96        Ok(message
97            .map(|message| ExpandedMacro {
98                name: message.name,
99                expansion: message.expansion,
100            })
101            .unwrap_or_default())
102    }
103
104    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
105        proto::LspExtExpandMacro {
106            project_id,
107            buffer_id: buffer.remote_id().into(),
108            position: Some(language::proto::serialize_anchor(
109                &buffer.anchor_before(self.position),
110            )),
111        }
112    }
113
114    async fn from_proto(
115        message: Self::ProtoRequest,
116        _: Entity<LspStore>,
117        buffer: Entity<Buffer>,
118        cx: AsyncApp,
119    ) -> anyhow::Result<Self> {
120        let position = message
121            .position
122            .and_then(deserialize_anchor)
123            .context("invalid position")?;
124        Ok(Self {
125            position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
126        })
127    }
128
129    fn response_to_proto(
130        response: ExpandedMacro,
131        _: &mut LspStore,
132        _: PeerId,
133        _: &clock::Global,
134        _: &mut App,
135    ) -> proto::LspExtExpandMacroResponse {
136        proto::LspExtExpandMacroResponse {
137            name: response.name,
138            expansion: response.expansion,
139        }
140    }
141
142    async fn response_from_proto(
143        self,
144        message: proto::LspExtExpandMacroResponse,
145        _: Entity<LspStore>,
146        _: Entity<Buffer>,
147        _: AsyncApp,
148    ) -> anyhow::Result<ExpandedMacro> {
149        Ok(ExpandedMacro {
150            name: message.name,
151            expansion: message.expansion,
152        })
153    }
154
155    fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> Result<BufferId> {
156        BufferId::new(message.buffer_id)
157    }
158}
159
160pub enum LspOpenDocs {}
161
162impl lsp::request::Request for LspOpenDocs {
163    type Params = OpenDocsParams;
164    type Result = Option<DocsUrls>;
165    const METHOD: &'static str = "experimental/externalDocs";
166}
167
168#[derive(Serialize, Deserialize, Debug)]
169#[serde(rename_all = "camelCase")]
170pub struct OpenDocsParams {
171    pub text_document: lsp::TextDocumentIdentifier,
172    pub position: lsp::Position,
173}
174
175#[derive(Serialize, Deserialize, Debug, Default)]
176#[serde(rename_all = "camelCase")]
177pub struct DocsUrls {
178    pub web: Option<String>,
179    pub local: Option<String>,
180}
181
182impl DocsUrls {
183    pub fn is_empty(&self) -> bool {
184        self.web.is_none() && self.local.is_none()
185    }
186}
187
188#[derive(Debug)]
189pub struct OpenDocs {
190    pub position: PointUtf16,
191}
192
193#[async_trait(?Send)]
194impl LspCommand for OpenDocs {
195    type Response = DocsUrls;
196    type LspRequest = LspOpenDocs;
197    type ProtoRequest = proto::LspExtOpenDocs;
198
199    fn display_name(&self) -> &str {
200        "Open docs"
201    }
202
203    fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
204        true
205    }
206
207    fn to_lsp(
208        &self,
209        path: &Path,
210        _: &Buffer,
211        _: &Arc<LanguageServer>,
212        _: &App,
213    ) -> Result<OpenDocsParams> {
214        let uri = lsp::Uri::from_file_path(path)
215            .map_err(|()| anyhow::anyhow!("{path:?} is not a valid URI"))?;
216        Ok(OpenDocsParams {
217            text_document: lsp::TextDocumentIdentifier { uri },
218            position: point_to_lsp(self.position),
219        })
220    }
221
222    async fn response_from_lsp(
223        self,
224        message: Option<DocsUrls>,
225        _: Entity<LspStore>,
226        _: Entity<Buffer>,
227        _: LanguageServerId,
228        _: AsyncApp,
229    ) -> anyhow::Result<DocsUrls> {
230        Ok(message
231            .map(|message| DocsUrls {
232                web: message.web,
233                local: message.local,
234            })
235            .unwrap_or_default())
236    }
237
238    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs {
239        proto::LspExtOpenDocs {
240            project_id,
241            buffer_id: buffer.remote_id().into(),
242            position: Some(language::proto::serialize_anchor(
243                &buffer.anchor_before(self.position),
244            )),
245        }
246    }
247
248    async fn from_proto(
249        message: Self::ProtoRequest,
250        _: Entity<LspStore>,
251        buffer: Entity<Buffer>,
252        cx: AsyncApp,
253    ) -> anyhow::Result<Self> {
254        let position = message
255            .position
256            .and_then(deserialize_anchor)
257            .context("invalid position")?;
258        Ok(Self {
259            position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
260        })
261    }
262
263    fn response_to_proto(
264        response: DocsUrls,
265        _: &mut LspStore,
266        _: PeerId,
267        _: &clock::Global,
268        _: &mut App,
269    ) -> proto::LspExtOpenDocsResponse {
270        proto::LspExtOpenDocsResponse {
271            web: response.web,
272            local: response.local,
273        }
274    }
275
276    async fn response_from_proto(
277        self,
278        message: proto::LspExtOpenDocsResponse,
279        _: Entity<LspStore>,
280        _: Entity<Buffer>,
281        _: AsyncApp,
282    ) -> anyhow::Result<DocsUrls> {
283        Ok(DocsUrls {
284            web: message.web,
285            local: message.local,
286        })
287    }
288
289    fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result<BufferId> {
290        BufferId::new(message.buffer_id)
291    }
292}
293
294pub enum LspSwitchSourceHeader {}
295
296impl lsp::request::Request for LspSwitchSourceHeader {
297    type Params = SwitchSourceHeaderParams;
298    type Result = Option<SwitchSourceHeaderResult>;
299    const METHOD: &'static str = "textDocument/switchSourceHeader";
300}
301
302#[derive(Serialize, Deserialize, Debug)]
303#[serde(rename_all = "camelCase")]
304pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier);
305
306#[derive(Serialize, Deserialize, Debug, Default)]
307#[serde(rename_all = "camelCase")]
308pub struct SwitchSourceHeaderResult(pub String);
309
310#[derive(Default, Deserialize, Serialize, Debug)]
311#[serde(rename_all = "camelCase")]
312pub struct SwitchSourceHeader;
313
314#[derive(Debug)]
315pub struct GoToParentModule {
316    pub position: PointUtf16,
317}
318
319pub struct LspGoToParentModule {}
320
321impl lsp::request::Request for LspGoToParentModule {
322    type Params = lsp::TextDocumentPositionParams;
323    type Result = Option<Vec<lsp::LocationLink>>;
324    const METHOD: &'static str = "experimental/parentModule";
325}
326
327#[async_trait(?Send)]
328impl LspCommand for SwitchSourceHeader {
329    type Response = SwitchSourceHeaderResult;
330    type LspRequest = LspSwitchSourceHeader;
331    type ProtoRequest = proto::LspExtSwitchSourceHeader;
332
333    fn display_name(&self) -> &str {
334        "Switch source header"
335    }
336
337    fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
338        true
339    }
340
341    fn to_lsp(
342        &self,
343        path: &Path,
344        _: &Buffer,
345        _: &Arc<LanguageServer>,
346        _: &App,
347    ) -> Result<SwitchSourceHeaderParams> {
348        Ok(SwitchSourceHeaderParams(make_text_document_identifier(
349            path,
350        )?))
351    }
352
353    async fn response_from_lsp(
354        self,
355        message: Option<SwitchSourceHeaderResult>,
356        _: Entity<LspStore>,
357        _: Entity<Buffer>,
358        _: LanguageServerId,
359        _: AsyncApp,
360    ) -> anyhow::Result<SwitchSourceHeaderResult> {
361        Ok(message
362            .map(|message| SwitchSourceHeaderResult(message.0))
363            .unwrap_or_default())
364    }
365
366    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader {
367        proto::LspExtSwitchSourceHeader {
368            project_id,
369            buffer_id: buffer.remote_id().into(),
370        }
371    }
372
373    async fn from_proto(
374        _: Self::ProtoRequest,
375        _: Entity<LspStore>,
376        _: Entity<Buffer>,
377        _: AsyncApp,
378    ) -> anyhow::Result<Self> {
379        Ok(Self {})
380    }
381
382    fn response_to_proto(
383        response: SwitchSourceHeaderResult,
384        _: &mut LspStore,
385        _: PeerId,
386        _: &clock::Global,
387        _: &mut App,
388    ) -> proto::LspExtSwitchSourceHeaderResponse {
389        proto::LspExtSwitchSourceHeaderResponse {
390            target_file: response.0,
391        }
392    }
393
394    async fn response_from_proto(
395        self,
396        message: proto::LspExtSwitchSourceHeaderResponse,
397        _: Entity<LspStore>,
398        _: Entity<Buffer>,
399        _: AsyncApp,
400    ) -> anyhow::Result<SwitchSourceHeaderResult> {
401        Ok(SwitchSourceHeaderResult(message.target_file))
402    }
403
404    fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result<BufferId> {
405        BufferId::new(message.buffer_id)
406    }
407}
408
409#[async_trait(?Send)]
410impl LspCommand for GoToParentModule {
411    type Response = Vec<LocationLink>;
412    type LspRequest = LspGoToParentModule;
413    type ProtoRequest = proto::LspExtGoToParentModule;
414
415    fn display_name(&self) -> &str {
416        "Go to parent module"
417    }
418
419    fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
420        true
421    }
422
423    fn to_lsp(
424        &self,
425        path: &Path,
426        _: &Buffer,
427        _: &Arc<LanguageServer>,
428        _: &App,
429    ) -> Result<lsp::TextDocumentPositionParams> {
430        make_lsp_text_document_position(path, self.position)
431    }
432
433    async fn response_from_lsp(
434        self,
435        links: Option<Vec<lsp::LocationLink>>,
436        lsp_store: Entity<LspStore>,
437        buffer: Entity<Buffer>,
438        server_id: LanguageServerId,
439        cx: AsyncApp,
440    ) -> anyhow::Result<Vec<LocationLink>> {
441        location_links_from_lsp(
442            links.map(lsp::GotoDefinitionResponse::Link),
443            lsp_store,
444            buffer,
445            server_id,
446            cx,
447        )
448        .await
449    }
450
451    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtGoToParentModule {
452        proto::LspExtGoToParentModule {
453            project_id,
454            buffer_id: buffer.remote_id().to_proto(),
455            position: Some(language::proto::serialize_anchor(
456                &buffer.anchor_before(self.position),
457            )),
458        }
459    }
460
461    async fn from_proto(
462        request: Self::ProtoRequest,
463        _: Entity<LspStore>,
464        buffer: Entity<Buffer>,
465        cx: AsyncApp,
466    ) -> anyhow::Result<Self> {
467        let position = request
468            .position
469            .and_then(deserialize_anchor)
470            .context("bad request with bad position")?;
471        Ok(Self {
472            position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
473        })
474    }
475
476    fn response_to_proto(
477        links: Vec<LocationLink>,
478        lsp_store: &mut LspStore,
479        peer_id: PeerId,
480        _: &clock::Global,
481        cx: &mut App,
482    ) -> proto::LspExtGoToParentModuleResponse {
483        proto::LspExtGoToParentModuleResponse {
484            links: location_links_to_proto(links, lsp_store, peer_id, cx),
485        }
486    }
487
488    async fn response_from_proto(
489        self,
490        message: proto::LspExtGoToParentModuleResponse,
491        lsp_store: Entity<LspStore>,
492        _: Entity<Buffer>,
493        cx: AsyncApp,
494    ) -> anyhow::Result<Vec<LocationLink>> {
495        location_links_from_proto(message.links, lsp_store, cx).await
496    }
497
498    fn buffer_id_from_proto(message: &proto::LspExtGoToParentModule) -> Result<BufferId> {
499        BufferId::new(message.buffer_id)
500    }
501}
502
503// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
504// Taken from https://github.com/rust-lang/rust-analyzer/blob/3aaa35b49ef27e15144952aa4f7ba3eecd36fbb4/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
505//
506// Note that in rust-analyzer, `Runnable` is defined as:
507//
508// ```
509// #[derive(Deserialize, Serialize, Debug, Clone)]
510// #[serde(rename_all = "camelCase")]
511// pub struct Runnable {
512//     pub label: String,
513//     #[serde(skip_serializing_if = "Option::is_none")]
514//     pub location: Option<lsp_types::LocationLink>,
515//     pub kind: RunnableKind,
516//     pub args: RunnableArgs,
517// }
518//
519// #[derive(Deserialize, Serialize, Debug, Clone)]
520// #[serde(rename_all = "camelCase")]
521// #[serde(untagged)]
522// pub enum RunnableArgs {
523//     Cargo(CargoRunnableArgs),
524//     Shell(ShellRunnableArgs),
525// }
526// ```
527//
528// i.e., RunnableArgs uses serde(untagged) and is not associated with
529// RunnableKind. But rust-analyzer always syncs RunnableKind with RunnableArgs:
530//
531// * https://github.com/rust-lang/rust-analyzer/blob/3aaa35b49ef27e15144952aa4f7ba3eecd36fbb4/crates/rust-analyzer/src/lsp/to_proto.rs#L1608-L1633
532// * https://github.com/rust-lang/rust-analyzer/blob/3aaa35b49ef27e15144952aa4f7ba3eecd36fbb4/crates/rust-analyzer/src/lsp/to_proto.rs#L1648-L1653
533// * https://github.com/rust-lang/rust-analyzer/blob/3aaa35b49ef27e15144952aa4f7ba3eecd36fbb4/crates/rust-analyzer/src/handlers/request.rs#L1052-L1066
534//
535// And it really doesn't make any sense for it to be any other way. On top of
536// that, the Shell and Cargo variants are similar enough that serde(untagged)
537// deserialization has been observed to confuse one for the other. So we rely on
538// RunnableKind to determine which variant to deserialize.
539pub enum Runnables {}
540
541impl lsp::request::Request for Runnables {
542    type Params = RunnablesParams;
543    type Result = Vec<Runnable>;
544    const METHOD: &'static str = "experimental/runnables";
545}
546
547#[derive(Serialize, Deserialize, Debug, Clone)]
548#[serde(rename_all = "camelCase")]
549pub struct RunnablesParams {
550    pub text_document: lsp::TextDocumentIdentifier,
551    #[serde(default)]
552    pub position: Option<lsp::Position>,
553}
554
555#[derive(Deserialize, Serialize, Debug, Clone)]
556#[serde(rename_all = "camelCase")]
557pub struct Runnable {
558    pub label: String,
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub location: Option<lsp::LocationLink>,
561    #[serde(flatten)]
562    pub args: RunnableArgs,
563}
564
565/// The `kind` field in the JSON determines which variant is deserialized; see
566/// comment on `Runnables` above for more discussion.
567#[derive(Deserialize, Serialize, Debug, Clone)]
568#[serde(tag = "kind", content = "args")]
569#[serde(rename_all = "lowercase")]
570pub enum RunnableArgs {
571    Cargo(CargoRunnableArgs),
572    Shell(ShellRunnableArgs),
573}
574
575#[derive(Deserialize, Serialize, Debug, Clone)]
576#[serde(rename_all = "camelCase")]
577pub struct CargoRunnableArgs {
578    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
579    pub environment: HashMap<String, String>,
580    pub cwd: PathBuf,
581    /// Command to be executed instead of cargo
582    #[serde(default)]
583    pub override_cargo: Option<String>,
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub workspace_root: Option<PathBuf>,
586    // command, --package and --lib stuff
587    #[serde(default)]
588    pub cargo_args: Vec<String>,
589    // stuff after --
590    #[serde(default)]
591    pub executable_args: Vec<String>,
592}
593
594#[derive(Deserialize, Serialize, Debug, Clone)]
595#[serde(rename_all = "camelCase")]
596pub struct ShellRunnableArgs {
597    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
598    pub environment: HashMap<String, String>,
599    pub cwd: PathBuf,
600    pub program: String,
601    #[serde(default)]
602    pub args: Vec<String>,
603}
604
605#[derive(Debug)]
606pub struct GetLspRunnables {
607    pub buffer_id: BufferId,
608    pub position: Option<text::Anchor>,
609}
610
611#[derive(Debug, Default)]
612pub struct LspRunnables {
613    pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
614}
615
616pub fn runnable_to_task_template(label: String, args: RunnableArgs) -> TaskTemplate {
617    let mut task_template = TaskTemplate::default();
618    task_template.label = label;
619    match args {
620        RunnableArgs::Cargo(cargo) => {
621            match cargo.override_cargo {
622                Some(override_cargo) => {
623                    let mut override_parts = override_cargo.split(" ").map(|s| s.to_string());
624                    task_template.command = override_parts
625                        .next()
626                        .unwrap_or_else(|| override_cargo.clone());
627                    task_template.args.extend(override_parts);
628                }
629                None => task_template.command = "cargo".to_string(),
630            };
631            task_template.env = cargo.environment;
632            task_template.cwd = Some(
633                cargo
634                    .workspace_root
635                    .unwrap_or(cargo.cwd)
636                    .to_string_lossy()
637                    .to_string(),
638            );
639            task_template.args.extend(cargo.cargo_args);
640            if !cargo.executable_args.is_empty() {
641                let shell_kind = task_template.shell.shell_kind(cfg!(windows));
642                task_template.args.push("--".to_string());
643                task_template.args.extend(
644                    cargo
645                        .executable_args
646                        .into_iter()
647                        // rust-analyzer's doctest data may contain things like `X<T>::new`
648                        // which cause shell issues when run as `$SHELL -i -c "cargo test ..."`.
649                        // Escape extra cargo args unconditionally as those are unlikely to contain `~`.
650                        .flat_map(|extra_arg| {
651                            shell_kind.try_quote(&extra_arg).map(|s| s.to_string())
652                        }),
653                );
654            }
655        }
656        RunnableArgs::Shell(shell) => {
657            task_template.command = shell.program;
658            task_template.args = shell.args;
659            task_template.env = shell.environment;
660            task_template.cwd = Some(shell.cwd.to_string_lossy().into_owned());
661        }
662    }
663    task_template
664}
665
666#[async_trait(?Send)]
667impl LspCommand for GetLspRunnables {
668    type Response = LspRunnables;
669    type LspRequest = Runnables;
670    type ProtoRequest = proto::LspExtRunnables;
671
672    fn display_name(&self) -> &str {
673        "LSP Runnables"
674    }
675
676    fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
677        true
678    }
679
680    fn to_lsp(
681        &self,
682        path: &Path,
683        buffer: &Buffer,
684        _: &Arc<LanguageServer>,
685        _: &App,
686    ) -> Result<RunnablesParams> {
687        let url = file_path_to_lsp_url(path)?;
688        Ok(RunnablesParams {
689            text_document: lsp::TextDocumentIdentifier::new(url),
690            position: self
691                .position
692                .map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
693        })
694    }
695
696    async fn response_from_lsp(
697        self,
698        lsp_runnables: Vec<Runnable>,
699        lsp_store: Entity<LspStore>,
700        buffer: Entity<Buffer>,
701        server_id: LanguageServerId,
702        mut cx: AsyncApp,
703    ) -> Result<LspRunnables> {
704        let mut runnables = Vec::with_capacity(lsp_runnables.len());
705
706        for runnable in lsp_runnables {
707            let location = match runnable.location {
708                Some(location) => Some(
709                    location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
710                        .await?,
711                ),
712                None => None,
713            };
714            let task_template = runnable_to_task_template(runnable.label, runnable.args);
715            runnables.push((location, task_template));
716        }
717
718        Ok(LspRunnables { runnables })
719    }
720
721    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
722        proto::LspExtRunnables {
723            project_id,
724            buffer_id: buffer.remote_id().to_proto(),
725            position: self.position.as_ref().map(serialize_anchor),
726        }
727    }
728
729    async fn from_proto(
730        message: proto::LspExtRunnables,
731        _: Entity<LspStore>,
732        _: Entity<Buffer>,
733        _: AsyncApp,
734    ) -> Result<Self> {
735        let buffer_id = Self::buffer_id_from_proto(&message)?;
736        let position = message.position.and_then(deserialize_anchor);
737        Ok(Self {
738            buffer_id,
739            position,
740        })
741    }
742
743    fn response_to_proto(
744        response: LspRunnables,
745        lsp_store: &mut LspStore,
746        peer_id: PeerId,
747        _: &clock::Global,
748        cx: &mut App,
749    ) -> proto::LspExtRunnablesResponse {
750        proto::LspExtRunnablesResponse {
751            runnables: response
752                .runnables
753                .into_iter()
754                .map(|(location, task_template)| proto::LspRunnable {
755                    location: location
756                        .map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
757                    task_template: serde_json::to_vec(&task_template).unwrap(),
758                })
759                .collect(),
760        }
761    }
762
763    async fn response_from_proto(
764        self,
765        message: proto::LspExtRunnablesResponse,
766        lsp_store: Entity<LspStore>,
767        _: Entity<Buffer>,
768        mut cx: AsyncApp,
769    ) -> Result<LspRunnables> {
770        let mut runnables = LspRunnables {
771            runnables: Vec::new(),
772        };
773
774        for lsp_runnable in message.runnables {
775            let location = match lsp_runnable.location {
776                Some(location) => {
777                    Some(location_link_from_proto(location, lsp_store.clone(), &mut cx).await?)
778                }
779                None => None,
780            };
781            let task_template = serde_json::from_slice(&lsp_runnable.task_template)
782                .context("deserializing task template from proto")?;
783            runnables.runnables.push((location, task_template));
784        }
785
786        Ok(runnables)
787    }
788
789    fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
790        BufferId::new(message.buffer_id)
791    }
792}
793
794#[derive(Debug)]
795pub struct LspExtCancelFlycheck {}
796
797#[derive(Debug)]
798pub struct LspExtRunFlycheck {}
799
800#[derive(Debug)]
801pub struct LspExtClearFlycheck {}
802
803impl lsp::notification::Notification for LspExtCancelFlycheck {
804    type Params = ();
805    const METHOD: &'static str = "rust-analyzer/cancelFlycheck";
806}
807
808impl lsp::notification::Notification for LspExtRunFlycheck {
809    type Params = RunFlycheckParams;
810    const METHOD: &'static str = "rust-analyzer/runFlycheck";
811}
812
813#[derive(Deserialize, Serialize, Debug)]
814#[serde(rename_all = "camelCase")]
815pub struct RunFlycheckParams {
816    pub text_document: Option<lsp::TextDocumentIdentifier>,
817}
818
819impl lsp::notification::Notification for LspExtClearFlycheck {
820    type Params = ();
821    const METHOD: &'static str = "rust-analyzer/clearFlycheck";
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827
828    #[test]
829    fn shell_runnable_deserializes_as_shell() {
830        // rust-analyzer sends this when `runnables.test.overrideCommand` is
831        // configured (e.g. for nextest).
832        let json = serde_json::json!({
833            "label": "test my_test",
834            "kind": "shell",
835            "args": {
836                "environment": {"RUSTC_TOOLCHAIN": "/path/to/toolchain"},
837                "cwd": "/project",
838                "program": "cargo",
839                "args": ["nextest", "run", "--package", "my-crate", "--lib", "--", "my_test", "--exact", "--include-ignored"]
840            }
841        });
842
843        let runnable: Runnable =
844            serde_json::from_value(json).expect("shell runnable should deserialize");
845        let RunnableArgs::Shell(shell) = &runnable.args else {
846            panic!("expected Shell variant, got {:?}", runnable.args);
847        };
848        assert_eq!(shell.program, "cargo");
849        assert_eq!(shell.args[0], "nextest");
850        assert_eq!(shell.args[1], "run");
851    }
852
853    #[test]
854    fn cargo_runnable_deserializes_as_cargo() {
855        // Standard cargo runnable from rust-analyzer.
856        let json = serde_json::json!({
857            "label": "cargo test -p my-crate",
858            "kind": "cargo",
859            "args": {
860                "environment": {},
861                "cwd": "/project",
862                "overrideCargo": null,
863                "workspaceRoot": "/project",
864                "cargoArgs": ["test", "--package", "my-crate", "--lib"],
865                "executableArgs": ["my_test", "--exact"]
866            }
867        });
868
869        let runnable: Runnable =
870            serde_json::from_value(json).expect("cargo runnable should deserialize");
871        let RunnableArgs::Cargo(cargo) = &runnable.args else {
872            panic!("expected Cargo variant, got {:?}", runnable.args);
873        };
874        assert_eq!(
875            cargo.cargo_args,
876            vec!["test", "--package", "my-crate", "--lib"]
877        );
878        assert_eq!(cargo.executable_args, vec!["my_test", "--exact"]);
879    }
880}
881
Served at tenant.openagents/omega Member data and write actions are omitted.