Skip to repository content881 lines · 26.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:57:42.293Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
lsp_ext_command.rs
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