1use anyhow::{anyhow, Context, Result};
2use assistant_slash_command::{
3 ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
4 SlashCommandResult,
5};
6use fs::Fs;
7use gpui::{AppContext, Model, Task, WeakView};
8use language::{BufferSnapshot, LspAdapterDelegate};
9use project::{Project, ProjectPath};
10use std::{
11 fmt::Write,
12 path::Path,
13 sync::{atomic::AtomicBool, Arc},
14};
15use ui::prelude::*;
16use workspace::Workspace;
17
18pub(crate) 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: Model<Project>, cx: &mut AppContext) -> 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<WeakView<Workspace>>,
111 _cx: &mut WindowContext,
112 ) -> Task<Result<Vec<ArgumentCompletion>>> {
113 Task::ready(Err(anyhow!("this command does not require argument")))
114 }
115
116 fn requires_argument(&self) -> bool {
117 false
118 }
119
120 fn run(
121 self: Arc<Self>,
122 _arguments: &[String],
123 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
124 _context_buffer: BufferSnapshot,
125 workspace: WeakView<Workspace>,
126 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
127 cx: &mut WindowContext,
128 ) -> Task<SlashCommandResult> {
129 let output = workspace.update(cx, |workspace, cx| {
130 let project = workspace.project().clone();
131 let fs = workspace.project().read(cx).fs().clone();
132 let path = Self::path_to_cargo_toml(project, cx);
133 let output = cx.background_executor().spawn(async move {
134 let path = path.with_context(|| "Cargo.toml not found")?;
135 Self::build_message(fs, &path).await
136 });
137
138 cx.foreground_executor().spawn(async move {
139 let text = output.await?;
140 let range = 0..text.len();
141 Ok(SlashCommandOutput {
142 text,
143 sections: vec![SlashCommandOutputSection {
144 range,
145 icon: IconName::FileTree,
146 label: "Project".into(),
147 metadata: None,
148 }],
149 run_commands_in_text: false,
150 }
151 .to_event_stream())
152 })
153 });
154 output.unwrap_or_else(|error| Task::ready(Err(error)))
155 }
156}