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}