project_command.rs

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