1use std::ops::Range;
2
3use file_icons::FileIcons;
4use gpui::{App, Entity, SharedString};
5use language::Buffer;
6use language_model::{LanguageModelRequestMessage, MessageContent};
7use project::ProjectPath;
8use serde::{Deserialize, Serialize};
9use text::{Anchor, BufferId};
10use ui::IconName;
11use util::post_inc;
12
13use crate::{context_store::buffer_path_log_err, thread::Thread};
14
15#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
16pub struct ContextId(pub(crate) usize);
17
18impl ContextId {
19 pub fn post_inc(&mut self) -> Self {
20 Self(post_inc(&mut self.0))
21 }
22}
23
24/// Some context attached to a message in a thread.
25#[derive(Debug, Clone)]
26pub struct ContextSnapshot {
27 pub id: ContextId,
28 pub name: SharedString,
29 pub parent: Option<SharedString>,
30 pub tooltip: Option<SharedString>,
31 pub icon_path: Option<SharedString>,
32 pub kind: ContextKind,
33 /// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
34 pub text: Box<[SharedString]>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ContextKind {
39 File,
40 Directory,
41 Symbol,
42 FetchedUrl,
43 Thread,
44}
45
46impl ContextKind {
47 pub fn icon(&self) -> IconName {
48 match self {
49 ContextKind::File => IconName::File,
50 ContextKind::Directory => IconName::Folder,
51 ContextKind::Symbol => IconName::Code,
52 ContextKind::FetchedUrl => IconName::Globe,
53 ContextKind::Thread => IconName::MessageBubbles,
54 }
55 }
56}
57
58#[derive(Debug)]
59pub enum AssistantContext {
60 File(FileContext),
61 Directory(DirectoryContext),
62 Symbol(SymbolContext),
63 FetchedUrl(FetchedUrlContext),
64 Thread(ThreadContext),
65}
66
67impl AssistantContext {
68 pub fn id(&self) -> ContextId {
69 match self {
70 Self::File(file) => file.id,
71 Self::Directory(directory) => directory.snapshot.id,
72 Self::Symbol(symbol) => symbol.id,
73 Self::FetchedUrl(url) => url.id,
74 Self::Thread(thread) => thread.id,
75 }
76 }
77}
78
79#[derive(Debug)]
80pub struct FileContext {
81 pub id: ContextId,
82 pub context_buffer: ContextBuffer,
83}
84
85#[derive(Debug)]
86pub struct DirectoryContext {
87 pub path: ProjectPath,
88 pub context_buffers: Vec<ContextBuffer>,
89 pub snapshot: ContextSnapshot,
90}
91
92#[derive(Debug)]
93pub struct SymbolContext {
94 pub id: ContextId,
95 pub context_symbol: ContextSymbol,
96}
97
98#[derive(Debug)]
99pub struct FetchedUrlContext {
100 pub id: ContextId,
101 pub url: SharedString,
102 pub text: SharedString,
103}
104
105// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
106// explicitly or have a WeakModel<Thread> and remove during snapshot.
107
108#[derive(Debug)]
109pub struct ThreadContext {
110 pub id: ContextId,
111 pub thread: Entity<Thread>,
112 pub text: SharedString,
113}
114
115// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
116// the context from the message editor in this case.
117
118#[derive(Debug, Clone)]
119pub struct ContextBuffer {
120 pub id: BufferId,
121 pub buffer: Entity<Buffer>,
122 pub version: clock::Global,
123 pub text: SharedString,
124}
125
126#[derive(Debug, Clone)]
127pub struct ContextSymbol {
128 pub id: ContextSymbolId,
129 pub buffer: Entity<Buffer>,
130 pub buffer_version: clock::Global,
131 /// The range that the symbol encloses, e.g. for function symbol, this will
132 /// include not only the signature, but also the body
133 pub enclosing_range: Range<Anchor>,
134 pub text: SharedString,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138pub struct ContextSymbolId {
139 pub path: ProjectPath,
140 pub name: SharedString,
141 pub range: Range<Anchor>,
142}
143
144impl AssistantContext {
145 pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
146 match &self {
147 Self::File(file_context) => file_context.snapshot(cx),
148 Self::Directory(directory_context) => Some(directory_context.snapshot()),
149 Self::Symbol(symbol_context) => symbol_context.snapshot(cx),
150 Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
151 Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
152 }
153 }
154}
155
156impl FileContext {
157 pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
158 let buffer = self.context_buffer.buffer.read(cx);
159 let path = buffer_path_log_err(buffer)?;
160 let full_path: SharedString = path.to_string_lossy().into_owned().into();
161 let name = match path.file_name() {
162 Some(name) => name.to_string_lossy().into_owned().into(),
163 None => full_path.clone(),
164 };
165 let parent = path
166 .parent()
167 .and_then(|p| p.file_name())
168 .map(|p| p.to_string_lossy().into_owned().into());
169
170 let icon_path = FileIcons::get_icon(&path, cx);
171
172 Some(ContextSnapshot {
173 id: self.id,
174 name,
175 parent,
176 tooltip: Some(full_path),
177 icon_path,
178 kind: ContextKind::File,
179 text: Box::new([self.context_buffer.text.clone()]),
180 })
181 }
182}
183
184impl DirectoryContext {
185 pub fn new(
186 id: ContextId,
187 project_path: ProjectPath,
188 context_buffers: Vec<ContextBuffer>,
189 ) -> DirectoryContext {
190 let full_path: SharedString = project_path.path.to_string_lossy().into_owned().into();
191
192 let name = match project_path.path.file_name() {
193 Some(name) => name.to_string_lossy().into_owned().into(),
194 None => full_path.clone(),
195 };
196
197 let parent = project_path
198 .path
199 .parent()
200 .and_then(|p| p.file_name())
201 .map(|p| p.to_string_lossy().into_owned().into());
202
203 // TODO: include directory path in text?
204 let text = context_buffers
205 .iter()
206 .map(|b| b.text.clone())
207 .collect::<Vec<_>>()
208 .into();
209
210 DirectoryContext {
211 path: project_path,
212 context_buffers,
213 snapshot: ContextSnapshot {
214 id,
215 name,
216 parent,
217 tooltip: Some(full_path),
218 icon_path: None,
219 kind: ContextKind::Directory,
220 text,
221 },
222 }
223 }
224
225 pub fn snapshot(&self) -> ContextSnapshot {
226 self.snapshot.clone()
227 }
228}
229
230impl SymbolContext {
231 pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
232 let buffer = self.context_symbol.buffer.read(cx);
233 let name = self.context_symbol.id.name.clone();
234 let path = buffer_path_log_err(buffer)?
235 .to_string_lossy()
236 .into_owned()
237 .into();
238
239 Some(ContextSnapshot {
240 id: self.id,
241 name,
242 parent: Some(path),
243 tooltip: None,
244 icon_path: None,
245 kind: ContextKind::Symbol,
246 text: Box::new([self.context_symbol.text.clone()]),
247 })
248 }
249}
250
251impl FetchedUrlContext {
252 pub fn snapshot(&self) -> ContextSnapshot {
253 ContextSnapshot {
254 id: self.id,
255 name: self.url.clone(),
256 parent: None,
257 tooltip: None,
258 icon_path: None,
259 kind: ContextKind::FetchedUrl,
260 text: Box::new([self.text.clone()]),
261 }
262 }
263}
264
265impl ThreadContext {
266 pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
267 let thread = self.thread.read(cx);
268 ContextSnapshot {
269 id: self.id,
270 name: thread.summary().unwrap_or("New thread".into()),
271 parent: None,
272 tooltip: None,
273 icon_path: None,
274 kind: ContextKind::Thread,
275 text: Box::new([self.text.clone()]),
276 }
277 }
278}
279
280pub fn attach_context_to_message(
281 message: &mut LanguageModelRequestMessage,
282 contexts: impl Iterator<Item = ContextSnapshot>,
283) {
284 let mut file_context = Vec::new();
285 let mut directory_context = Vec::new();
286 let mut symbol_context = Vec::new();
287 let mut fetch_context = Vec::new();
288 let mut thread_context = Vec::new();
289
290 let mut capacity = 0;
291 for context in contexts {
292 capacity += context.text.len();
293 match context.kind {
294 ContextKind::File => file_context.push(context),
295 ContextKind::Directory => directory_context.push(context),
296 ContextKind::Symbol => symbol_context.push(context),
297 ContextKind::FetchedUrl => fetch_context.push(context),
298 ContextKind::Thread => thread_context.push(context),
299 }
300 }
301 if !file_context.is_empty() {
302 capacity += 1;
303 }
304 if !directory_context.is_empty() {
305 capacity += 1;
306 }
307 if !symbol_context.is_empty() {
308 capacity += 1;
309 }
310 if !fetch_context.is_empty() {
311 capacity += 1 + fetch_context.len();
312 }
313 if !thread_context.is_empty() {
314 capacity += 1 + thread_context.len();
315 }
316 if capacity == 0 {
317 return;
318 }
319
320 let mut context_chunks = Vec::with_capacity(capacity);
321
322 if !file_context.is_empty() {
323 context_chunks.push("The following files are available:\n");
324 for context in &file_context {
325 for chunk in &context.text {
326 context_chunks.push(&chunk);
327 }
328 }
329 }
330
331 if !directory_context.is_empty() {
332 context_chunks.push("The following directories are available:\n");
333 for context in &directory_context {
334 for chunk in &context.text {
335 context_chunks.push(&chunk);
336 }
337 }
338 }
339
340 if !symbol_context.is_empty() {
341 context_chunks.push("The following symbols are available:\n");
342 for context in &symbol_context {
343 for chunk in &context.text {
344 context_chunks.push(&chunk);
345 }
346 }
347 }
348
349 if !fetch_context.is_empty() {
350 context_chunks.push("The following fetched results are available:\n");
351 for context in &fetch_context {
352 context_chunks.push(&context.name);
353 for chunk in &context.text {
354 context_chunks.push(&chunk);
355 }
356 }
357 }
358
359 if !thread_context.is_empty() {
360 context_chunks.push("The following previous conversation threads are available:\n");
361 for context in &thread_context {
362 context_chunks.push(&context.name);
363 for chunk in &context.text {
364 context_chunks.push(&chunk);
365 }
366 }
367 }
368
369 debug_assert!(
370 context_chunks.len() == capacity,
371 "attach_context_message calculated capacity of {}, but length was {}",
372 capacity,
373 context_chunks.len()
374 );
375
376 if !context_chunks.is_empty() {
377 message
378 .content
379 .push(MessageContent::Text(context_chunks.join("\n")));
380 }
381}