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}