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}