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