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