current_project.rs

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