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}