current_project.rs

  1use std::path::{Path, PathBuf};
  2use std::sync::Arc;
  3use std::time::Duration;
  4
  5use anyhow::{anyhow, Result};
  6use fs::Fs;
  7use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
  8use project::{Project, ProjectPath};
  9use util::ResultExt;
 10
 11use crate::assistant_panel::Conversation;
 12use crate::{LanguageModelRequestMessage, Role};
 13
 14/// Ambient context about the current project.
 15pub struct CurrentProjectContext {
 16    pub enabled: bool,
 17    pub message: String,
 18    pub pending_message: Option<Task<()>>,
 19}
 20
 21#[allow(clippy::derivable_impls)]
 22impl Default for CurrentProjectContext {
 23    fn default() -> Self {
 24        Self {
 25            enabled: false,
 26            message: String::new(),
 27            pending_message: None,
 28        }
 29    }
 30}
 31
 32impl CurrentProjectContext {
 33    /// Returns the [`CurrentProjectContext`] as a message to the language model.
 34    pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
 35        self.enabled.then(|| LanguageModelRequestMessage {
 36            role: Role::System,
 37            content: self.message.clone(),
 38        })
 39    }
 40
 41    /// Updates the [`CurrentProjectContext`] for the given [`Project`].
 42    pub fn update(
 43        &mut self,
 44        fs: Arc<dyn Fs>,
 45        project: WeakModel<Project>,
 46        cx: &mut ModelContext<Conversation>,
 47    ) {
 48        if !self.enabled {
 49            self.message.clear();
 50            self.pending_message = None;
 51            cx.notify();
 52            return;
 53        }
 54
 55        self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
 56            const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
 57            cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
 58
 59            let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
 60            else {
 61                return;
 62            };
 63
 64            let Some(path_to_cargo_toml) = path_to_cargo_toml
 65                .ok_or_else(|| anyhow!("no Cargo.toml"))
 66                .log_err()
 67            else {
 68                return;
 69            };
 70
 71            let message_task = cx
 72                .background_executor()
 73                .spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
 74
 75            if let Some(message) = message_task.await.log_err() {
 76                conversation
 77                    .update(&mut cx, |conversation, _cx| {
 78                        conversation.ambient_context.current_project.message = message;
 79                    })
 80                    .log_err();
 81            }
 82        }));
 83    }
 84
 85    async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
 86        let buffer = fs.load(path_to_cargo_toml).await?;
 87        let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
 88
 89        let mut message = String::new();
 90
 91        let name = cargo_toml
 92            .package
 93            .as_ref()
 94            .map(|package| package.name.as_str());
 95        if let Some(name) = name {
 96            message.push_str(&format!(" named \"{name}\""));
 97        }
 98        message.push_str(". ");
 99
100        let description = cargo_toml
101            .package
102            .as_ref()
103            .and_then(|package| package.description.as_ref())
104            .and_then(|description| description.get().ok().cloned());
105        if let Some(description) = description.as_ref() {
106            message.push_str("It describes itself as ");
107            message.push_str(&format!("\"{description}\""));
108            message.push_str(". ");
109        }
110
111        let dependencies = cargo_toml.dependencies.keys().cloned().collect::<Vec<_>>();
112        if !dependencies.is_empty() {
113            message.push_str("The following dependencies are installed: ");
114            message.push_str(&dependencies.join(", "));
115            message.push_str(". ");
116        }
117
118        Ok(message)
119    }
120
121    fn path_to_cargo_toml(
122        project: WeakModel<Project>,
123        cx: &mut AsyncAppContext,
124    ) -> Result<Option<PathBuf>> {
125        cx.update(|cx| {
126            let worktree = project.update(cx, |project, _cx| {
127                project
128                    .worktrees()
129                    .next()
130                    .ok_or_else(|| anyhow!("no worktree"))
131            })??;
132
133            let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
134                let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
135                Some(ProjectPath {
136                    worktree_id: worktree.id(),
137                    path: cargo_toml.path.clone(),
138                })
139            });
140            let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
141                project
142                    .update(cx, |project, cx| project.absolute_path(&path, cx))
143                    .ok()
144                    .flatten()
145            });
146
147            Ok(path_to_cargo_toml)
148        })?
149    }
150}