1use anyhow::{Context as _, Result, anyhow};
2use assistant_slash_command::{
3 ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
4 SlashCommandResult,
5};
6use fs::Fs;
7use gpui::{App, Entity, Task, WeakEntity};
8use language::{BufferSnapshot, LspAdapterDelegate};
9use project::{Project, ProjectPath};
10use std::{
11 fmt::Write,
12 path::Path,
13 sync::{Arc, atomic::AtomicBool},
14};
15use ui::prelude::*;
16use workspace::Workspace;
17
18pub struct CargoWorkspaceSlashCommand;
19
20impl CargoWorkspaceSlashCommand {
21 async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
22 let buffer = fs.load(path_to_cargo_toml).await?;
23 let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
24
25 let mut message = String::new();
26 writeln!(message, "You are in a Rust project.")?;
27
28 if let Some(workspace) = cargo_toml.workspace {
29 writeln!(
30 message,
31 "The project is a Cargo workspace with the following members:"
32 )?;
33 for member in workspace.members {
34 writeln!(message, "- {member}")?;
35 }
36
37 if !workspace.default_members.is_empty() {
38 writeln!(message, "The default members are:")?;
39 for member in workspace.default_members {
40 writeln!(message, "- {member}")?;
41 }
42 }
43
44 if !workspace.dependencies.is_empty() {
45 writeln!(
46 message,
47 "The following workspace dependencies are installed:"
48 )?;
49 for dependency in workspace.dependencies.keys() {
50 writeln!(message, "- {dependency}")?;
51 }
52 }
53 } else if let Some(package) = cargo_toml.package {
54 writeln!(
55 message,
56 "The project name is \"{name}\".",
57 name = package.name
58 )?;
59
60 let description = package
61 .description
62 .as_ref()
63 .and_then(|description| description.get().ok().cloned());
64 if let Some(description) = description.as_ref() {
65 writeln!(message, "It describes itself as \"{description}\".")?;
66 }
67
68 if !cargo_toml.dependencies.is_empty() {
69 writeln!(message, "The following dependencies are installed:")?;
70 for dependency in cargo_toml.dependencies.keys() {
71 writeln!(message, "- {dependency}")?;
72 }
73 }
74 }
75
76 Ok(message)
77 }
78
79 fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
80 let worktree = project.read(cx).worktrees(cx).next()?;
81 let worktree = worktree.read(cx);
82 let entry = worktree.entry_for_path("Cargo.toml")?;
83 let path = ProjectPath {
84 worktree_id: worktree.id(),
85 path: entry.path.clone(),
86 };
87 Some(Arc::from(
88 project.read(cx).absolute_path(&path, cx)?.as_path(),
89 ))
90 }
91}
92
93impl SlashCommand for CargoWorkspaceSlashCommand {
94 fn name(&self) -> String {
95 "cargo-workspace".into()
96 }
97
98 fn description(&self) -> String {
99 "insert project workspace metadata".into()
100 }
101
102 fn menu_text(&self) -> String {
103 "Insert Project Workspace Metadata".into()
104 }
105
106 fn complete_argument(
107 self: Arc<Self>,
108 _arguments: &[String],
109 _cancel: Arc<AtomicBool>,
110 _workspace: Option<WeakEntity<Workspace>>,
111 _window: &mut Window,
112 _cx: &mut App,
113 ) -> Task<Result<Vec<ArgumentCompletion>>> {
114 Task::ready(Err(anyhow!("this command does not require argument")))
115 }
116
117 fn requires_argument(&self) -> bool {
118 false
119 }
120
121 fn run(
122 self: Arc<Self>,
123 _arguments: &[String],
124 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
125 _context_buffer: BufferSnapshot,
126 workspace: WeakEntity<Workspace>,
127 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
128 _window: &mut Window,
129 cx: &mut App,
130 ) -> Task<SlashCommandResult> {
131 let output = workspace.update(cx, |workspace, cx| {
132 let project = workspace.project().clone();
133 let fs = workspace.project().read(cx).fs().clone();
134 let path = Self::path_to_cargo_toml(project, cx);
135 let output = cx.background_spawn(async move {
136 let path = path.with_context(|| "Cargo.toml not found")?;
137 Self::build_message(fs, &path).await
138 });
139
140 cx.foreground_executor().spawn(async move {
141 let text = output.await?;
142 let range = 0..text.len();
143 Ok(SlashCommandOutput {
144 text,
145 sections: vec![SlashCommandOutputSection {
146 range,
147 icon: IconName::FileTree,
148 label: "Project".into(),
149 metadata: None,
150 }],
151 run_commands_in_text: false,
152 }
153 .into_event_stream())
154 })
155 });
156 output.unwrap_or_else(|error| Task::ready(Err(error)))
157 }
158}