1use std::{ops::Range, path::Path, sync::Arc};
2
3use gpui::{App, Entity, SharedString};
4use language::{Buffer, File};
5use language_model::LanguageModelRequestMessage;
6use project::{ProjectEntryId, ProjectPath, Worktree};
7use prompt_store::UserPromptId;
8use rope::Point;
9use serde::{Deserialize, Serialize};
10use text::{Anchor, BufferId};
11use ui::IconName;
12use util::post_inc;
13
14use crate::thread::Thread;
15
16pub const RULES_ICON: IconName = IconName::Context;
17
18#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
19pub struct ContextId(pub(crate) usize);
20
21impl ContextId {
22 pub fn post_inc(&mut self) -> Self {
23 Self(post_inc(&mut self.0))
24 }
25}
26
27pub enum ContextKind {
28 File,
29 Directory,
30 Symbol,
31 Excerpt,
32 FetchedUrl,
33 Thread,
34 Rules,
35}
36
37impl ContextKind {
38 pub fn icon(&self) -> IconName {
39 match self {
40 ContextKind::File => IconName::File,
41 ContextKind::Directory => IconName::Folder,
42 ContextKind::Symbol => IconName::Code,
43 ContextKind::Excerpt => IconName::Code,
44 ContextKind::FetchedUrl => IconName::Globe,
45 ContextKind::Thread => IconName::MessageBubbles,
46 ContextKind::Rules => RULES_ICON,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
52pub enum AssistantContext {
53 File(FileContext),
54 Directory(DirectoryContext),
55 Symbol(SymbolContext),
56 FetchedUrl(FetchedUrlContext),
57 Thread(ThreadContext),
58 Excerpt(ExcerptContext),
59 Rules(RulesContext),
60}
61
62impl AssistantContext {
63 pub fn id(&self) -> ContextId {
64 match self {
65 Self::File(file) => file.id,
66 Self::Directory(directory) => directory.id,
67 Self::Symbol(symbol) => symbol.id,
68 Self::FetchedUrl(url) => url.id,
69 Self::Thread(thread) => thread.id,
70 Self::Excerpt(excerpt) => excerpt.id,
71 Self::Rules(rules) => rules.id,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct FileContext {
78 pub id: ContextId,
79 pub context_buffer: ContextBuffer,
80}
81
82#[derive(Debug, Clone)]
83pub struct DirectoryContext {
84 pub id: ContextId,
85 pub worktree: Entity<Worktree>,
86 pub entry_id: ProjectEntryId,
87 pub last_path: Arc<Path>,
88 /// Buffers of the files within the directory.
89 pub context_buffers: Vec<ContextBuffer>,
90}
91
92impl DirectoryContext {
93 pub fn entry<'a>(&self, cx: &'a App) -> Option<&'a project::Entry> {
94 self.worktree.read(cx).entry_for_id(self.entry_id)
95 }
96
97 pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
98 let worktree = self.worktree.read(cx);
99 worktree
100 .entry_for_id(self.entry_id)
101 .map(|entry| ProjectPath {
102 worktree_id: worktree.id(),
103 path: entry.path.clone(),
104 })
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct SymbolContext {
110 pub id: ContextId,
111 pub context_symbol: ContextSymbol,
112}
113
114#[derive(Debug, Clone)]
115pub struct FetchedUrlContext {
116 pub id: ContextId,
117 pub url: SharedString,
118 pub text: SharedString,
119}
120
121#[derive(Debug, Clone)]
122pub struct ThreadContext {
123 pub id: ContextId,
124 // TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
125 // a WeakEntity and handle removal from the UI when it has dropped.
126 pub thread: Entity<Thread>,
127 pub text: SharedString,
128}
129
130impl ThreadContext {
131 pub fn summary(&self, cx: &App) -> SharedString {
132 self.thread
133 .read(cx)
134 .summary()
135 .unwrap_or("New thread".into())
136 }
137}
138
139#[derive(Clone)]
140pub struct ContextBuffer {
141 pub id: BufferId,
142 // TODO: Entity<Buffer> holds onto the buffer even if the buffer is deleted. Should probably be
143 // a WeakEntity and handle removal from the UI when it has dropped.
144 pub buffer: Entity<Buffer>,
145 pub file: Arc<dyn File>,
146 pub version: clock::Global,
147 pub text: SharedString,
148}
149
150impl std::fmt::Debug for ContextBuffer {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("ContextBuffer")
153 .field("id", &self.id)
154 .field("buffer", &self.buffer)
155 .field("version", &self.version)
156 .field("text", &self.text)
157 .finish()
158 }
159}
160
161#[derive(Debug, Clone)]
162pub struct ContextSymbol {
163 pub id: ContextSymbolId,
164 pub buffer: Entity<Buffer>,
165 pub buffer_version: clock::Global,
166 /// The range that the symbol encloses, e.g. for function symbol, this will
167 /// include not only the signature, but also the body
168 pub enclosing_range: Range<Anchor>,
169 pub text: SharedString,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173pub struct ContextSymbolId {
174 pub path: ProjectPath,
175 pub name: SharedString,
176 pub range: Range<Anchor>,
177}
178
179#[derive(Debug, Clone)]
180pub struct ExcerptContext {
181 pub id: ContextId,
182 pub range: Range<Anchor>,
183 pub line_range: Range<Point>,
184 pub context_buffer: ContextBuffer,
185}
186
187#[derive(Debug, Clone)]
188pub struct RulesContext {
189 pub id: ContextId,
190 pub prompt_id: UserPromptId,
191 pub title: SharedString,
192 pub text: SharedString,
193}
194
195/// Formats a collection of contexts into a string representation
196pub fn format_context_as_string<'a>(
197 contexts: impl Iterator<Item = &'a AssistantContext>,
198 cx: &App,
199) -> Option<String> {
200 let mut file_context = Vec::new();
201 let mut directory_context = Vec::new();
202 let mut symbol_context = Vec::new();
203 let mut excerpt_context = Vec::new();
204 let mut fetch_context = Vec::new();
205 let mut thread_context = Vec::new();
206 let mut rules_context = Vec::new();
207
208 for context in contexts {
209 match context {
210 AssistantContext::File(context) => file_context.push(context),
211 AssistantContext::Directory(context) => directory_context.push(context),
212 AssistantContext::Symbol(context) => symbol_context.push(context),
213 AssistantContext::Excerpt(context) => excerpt_context.push(context),
214 AssistantContext::FetchedUrl(context) => fetch_context.push(context),
215 AssistantContext::Thread(context) => thread_context.push(context),
216 AssistantContext::Rules(context) => rules_context.push(context),
217 }
218 }
219
220 if file_context.is_empty()
221 && directory_context.is_empty()
222 && symbol_context.is_empty()
223 && excerpt_context.is_empty()
224 && fetch_context.is_empty()
225 && thread_context.is_empty()
226 && rules_context.is_empty()
227 {
228 return None;
229 }
230
231 let mut result = String::new();
232 result.push_str("\n<context>\n\
233 The following items were attached by the user. You don't need to use other tools to read them.\n\n");
234
235 if !file_context.is_empty() {
236 result.push_str("<files>\n");
237 for context in file_context {
238 result.push_str(&context.context_buffer.text);
239 }
240 result.push_str("</files>\n");
241 }
242
243 if !directory_context.is_empty() {
244 result.push_str("<directories>\n");
245 for context in directory_context {
246 for context_buffer in &context.context_buffers {
247 result.push_str(&context_buffer.text);
248 }
249 }
250 result.push_str("</directories>\n");
251 }
252
253 if !symbol_context.is_empty() {
254 result.push_str("<symbols>\n");
255 for context in symbol_context {
256 result.push_str(&context.context_symbol.text);
257 result.push('\n');
258 }
259 result.push_str("</symbols>\n");
260 }
261
262 if !excerpt_context.is_empty() {
263 result.push_str("<excerpts>\n");
264 for context in excerpt_context {
265 result.push_str(&context.context_buffer.text);
266 result.push('\n');
267 }
268 result.push_str("</excerpts>\n");
269 }
270
271 if !fetch_context.is_empty() {
272 result.push_str("<fetched_urls>\n");
273 for context in &fetch_context {
274 result.push_str(&context.url);
275 result.push('\n');
276 result.push_str(&context.text);
277 result.push('\n');
278 }
279 result.push_str("</fetched_urls>\n");
280 }
281
282 if !thread_context.is_empty() {
283 result.push_str("<conversation_threads>\n");
284 for context in &thread_context {
285 result.push_str(&context.summary(cx));
286 result.push('\n');
287 result.push_str(&context.text);
288 result.push('\n');
289 }
290 result.push_str("</conversation_threads>\n");
291 }
292
293 if !rules_context.is_empty() {
294 result.push_str(
295 "<user_rules>\n\
296 The user has specified the following rules that should be applied:\n\n",
297 );
298 for context in &rules_context {
299 result.push_str(&context.text);
300 result.push('\n');
301 }
302 result.push_str("</user_rules>\n");
303 }
304
305 result.push_str("</context>\n");
306 Some(result)
307}
308
309pub fn attach_context_to_message<'a>(
310 message: &mut LanguageModelRequestMessage,
311 contexts: impl Iterator<Item = &'a AssistantContext>,
312 cx: &App,
313) {
314 if let Some(context_string) = format_context_as_string(contexts, cx) {
315 message.content.push(context_string.into());
316 }
317}