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