1use std::ops::Range;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::{Context as _, Result, anyhow};
6use collections::{BTreeMap, HashMap, HashSet};
7use futures::future::join_all;
8use futures::{self, Future, FutureExt, future};
9use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
10use language::{Buffer, File};
11use project::{ProjectItem, ProjectPath, Worktree};
12use rope::Rope;
13use text::{Anchor, BufferId, OffsetRangeExt};
14use util::{ResultExt as _, maybe};
15use workspace::Workspace;
16
17use crate::ThreadStore;
18use crate::context::{
19 AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
20 FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
21};
22use crate::context_strip::SuggestedContext;
23use crate::thread::{Thread, ThreadId};
24
25pub struct ContextStore {
26 workspace: WeakEntity<Workspace>,
27 context: Vec<AssistantContext>,
28 thread_store: Option<WeakEntity<ThreadStore>>,
29 // TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
30 next_context_id: ContextId,
31 files: BTreeMap<BufferId, ContextId>,
32 directories: HashMap<PathBuf, ContextId>,
33 symbols: HashMap<ContextSymbolId, ContextId>,
34 symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
35 symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
36 threads: HashMap<ThreadId, ContextId>,
37 thread_summary_tasks: Vec<Task<()>>,
38 fetched_urls: HashMap<String, ContextId>,
39}
40
41impl ContextStore {
42 pub fn new(
43 workspace: WeakEntity<Workspace>,
44 thread_store: Option<WeakEntity<ThreadStore>>,
45 ) -> Self {
46 Self {
47 workspace,
48 thread_store,
49 context: Vec::new(),
50 next_context_id: ContextId(0),
51 files: BTreeMap::default(),
52 directories: HashMap::default(),
53 symbols: HashMap::default(),
54 symbol_buffers: HashMap::default(),
55 symbols_by_path: HashMap::default(),
56 threads: HashMap::default(),
57 thread_summary_tasks: Vec::new(),
58 fetched_urls: HashMap::default(),
59 }
60 }
61
62 pub fn context(&self) -> &Vec<AssistantContext> {
63 &self.context
64 }
65
66 pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
67 self.context().iter().find(|context| context.id() == id)
68 }
69
70 pub fn clear(&mut self) {
71 self.context.clear();
72 self.files.clear();
73 self.directories.clear();
74 self.threads.clear();
75 self.fetched_urls.clear();
76 }
77
78 pub fn add_file_from_path(
79 &mut self,
80 project_path: ProjectPath,
81 remove_if_exists: bool,
82 cx: &mut Context<Self>,
83 ) -> Task<Result<()>> {
84 let workspace = self.workspace.clone();
85
86 let Some(project) = workspace
87 .upgrade()
88 .map(|workspace| workspace.read(cx).project().clone())
89 else {
90 return Task::ready(Err(anyhow!("failed to read project")));
91 };
92
93 cx.spawn(async move |this, cx| {
94 let open_buffer_task = project.update(cx, |project, cx| {
95 project.open_buffer(project_path.clone(), cx)
96 })?;
97
98 let buffer_entity = open_buffer_task.await?;
99 let buffer_id = this.update(cx, |_, cx| buffer_entity.read(cx).remote_id())?;
100
101 let already_included = this.update(cx, |this, _cx| {
102 match this.will_include_buffer(buffer_id, &project_path.path) {
103 Some(FileInclusion::Direct(context_id)) => {
104 if remove_if_exists {
105 this.remove_context(context_id);
106 }
107 true
108 }
109 Some(FileInclusion::InDirectory(_)) => true,
110 None => false,
111 }
112 })?;
113
114 if already_included {
115 return anyhow::Ok(());
116 }
117
118 let (buffer_info, text_task) = this.update(cx, |_, cx| {
119 let buffer = buffer_entity.read(cx);
120 collect_buffer_info_and_text(
121 project_path.path.clone(),
122 buffer_entity,
123 buffer,
124 None,
125 cx.to_async(),
126 )
127 })??;
128
129 let text = text_task.await;
130
131 this.update(cx, |this, _cx| {
132 this.insert_file(make_context_buffer(buffer_info, text));
133 })?;
134
135 anyhow::Ok(())
136 })
137 }
138
139 pub fn add_file_from_buffer(
140 &mut self,
141 buffer_entity: Entity<Buffer>,
142 cx: &mut Context<Self>,
143 ) -> Task<Result<()>> {
144 cx.spawn(async move |this, cx| {
145 let (buffer_info, text_task) = this.update(cx, |_, cx| {
146 let buffer = buffer_entity.read(cx);
147 let Some(file) = buffer.file() else {
148 return Err(anyhow!("Buffer has no path."));
149 };
150 collect_buffer_info_and_text(
151 file.path().clone(),
152 buffer_entity,
153 buffer,
154 None,
155 cx.to_async(),
156 )
157 })??;
158
159 let text = text_task.await;
160
161 this.update(cx, |this, _cx| {
162 this.insert_file(make_context_buffer(buffer_info, text))
163 })?;
164
165 anyhow::Ok(())
166 })
167 }
168
169 fn insert_file(&mut self, context_buffer: ContextBuffer) {
170 let id = self.next_context_id.post_inc();
171 self.files.insert(context_buffer.id, id);
172 self.context.push(AssistantContext::File(FileContext {
173 id,
174 context_buffer: context_buffer,
175 }));
176 }
177
178 pub fn add_directory(
179 &mut self,
180 project_path: ProjectPath,
181 remove_if_exists: bool,
182 cx: &mut Context<Self>,
183 ) -> Task<Result<()>> {
184 let workspace = self.workspace.clone();
185 let Some(project) = workspace
186 .upgrade()
187 .map(|workspace| workspace.read(cx).project().clone())
188 else {
189 return Task::ready(Err(anyhow!("failed to read project")));
190 };
191
192 let already_included = match self.includes_directory(&project_path.path) {
193 Some(FileInclusion::Direct(context_id)) => {
194 if remove_if_exists {
195 self.remove_context(context_id);
196 }
197 true
198 }
199 Some(FileInclusion::InDirectory(_)) => true,
200 None => false,
201 };
202 if already_included {
203 return Task::ready(Ok(()));
204 }
205
206 let worktree_id = project_path.worktree_id;
207 cx.spawn(async move |this, cx| {
208 let worktree = project.update(cx, |project, cx| {
209 project
210 .worktree_for_id(worktree_id, cx)
211 .ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
212 })??;
213
214 let files = worktree.update(cx, |worktree, _cx| {
215 collect_files_in_path(worktree, &project_path.path)
216 })?;
217
218 let open_buffers_task = project.update(cx, |project, cx| {
219 let tasks = files.iter().map(|file_path| {
220 project.open_buffer(
221 ProjectPath {
222 worktree_id,
223 path: file_path.clone(),
224 },
225 cx,
226 )
227 });
228 future::join_all(tasks)
229 })?;
230
231 let buffers = open_buffers_task.await;
232
233 let mut buffer_infos = Vec::new();
234 let mut text_tasks = Vec::new();
235 this.update(cx, |_, cx| {
236 for (path, buffer_entity) in files.into_iter().zip(buffers) {
237 // Skip all binary files and other non-UTF8 files
238 if let Ok(buffer_entity) = buffer_entity {
239 let buffer = buffer_entity.read(cx);
240 if let Some((buffer_info, text_task)) = collect_buffer_info_and_text(
241 path,
242 buffer_entity,
243 buffer,
244 None,
245 cx.to_async(),
246 )
247 .log_err()
248 {
249 buffer_infos.push(buffer_info);
250 text_tasks.push(text_task);
251 }
252 }
253 }
254 anyhow::Ok(())
255 })??;
256
257 let buffer_texts = future::join_all(text_tasks).await;
258 let context_buffers = buffer_infos
259 .into_iter()
260 .zip(buffer_texts)
261 .map(|(info, text)| make_context_buffer(info, text))
262 .collect::<Vec<_>>();
263
264 if context_buffers.is_empty() {
265 return Err(anyhow!(
266 "No text files found in {}",
267 &project_path.path.display()
268 ));
269 }
270
271 this.update(cx, |this, _| {
272 this.insert_directory(project_path, context_buffers);
273 })?;
274
275 anyhow::Ok(())
276 })
277 }
278
279 fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
280 let id = self.next_context_id.post_inc();
281 self.directories.insert(project_path.path.to_path_buf(), id);
282
283 self.context
284 .push(AssistantContext::Directory(DirectoryContext {
285 id,
286 project_path,
287 context_buffers,
288 }));
289 }
290
291 pub fn add_symbol(
292 &mut self,
293 buffer: Entity<Buffer>,
294 symbol_name: SharedString,
295 symbol_range: Range<Anchor>,
296 symbol_enclosing_range: Range<Anchor>,
297 remove_if_exists: bool,
298 cx: &mut Context<Self>,
299 ) -> Task<Result<bool>> {
300 let buffer_ref = buffer.read(cx);
301 let Some(file) = buffer_ref.file() else {
302 return Task::ready(Err(anyhow!("Buffer has no path.")));
303 };
304
305 let Some(project_path) = buffer_ref.project_path(cx) else {
306 return Task::ready(Err(anyhow!("Buffer has no project path.")));
307 };
308
309 if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
310 let mut matching_symbol_id = None;
311 for symbol in symbols_for_path {
312 if &symbol.name == &symbol_name {
313 let snapshot = buffer_ref.snapshot();
314 if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
315 matching_symbol_id = self.symbols.get(symbol).cloned();
316 break;
317 }
318 }
319 }
320
321 if let Some(id) = matching_symbol_id {
322 if remove_if_exists {
323 self.remove_context(id);
324 }
325 return Task::ready(Ok(false));
326 }
327 }
328
329 let (buffer_info, collect_content_task) = match collect_buffer_info_and_text(
330 file.path().clone(),
331 buffer,
332 buffer_ref,
333 Some(symbol_enclosing_range.clone()),
334 cx.to_async(),
335 ) {
336 Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
337 Err(err) => return Task::ready(Err(err)),
338 };
339
340 cx.spawn(async move |this, cx| {
341 let content = collect_content_task.await;
342
343 this.update(cx, |this, _cx| {
344 this.insert_symbol(make_context_symbol(
345 buffer_info,
346 project_path,
347 symbol_name,
348 symbol_range,
349 symbol_enclosing_range,
350 content,
351 ))
352 })?;
353 anyhow::Ok(true)
354 })
355 }
356
357 fn insert_symbol(&mut self, context_symbol: ContextSymbol) {
358 let id = self.next_context_id.post_inc();
359 self.symbols.insert(context_symbol.id.clone(), id);
360 self.symbols_by_path
361 .entry(context_symbol.id.path.clone())
362 .or_insert_with(Vec::new)
363 .push(context_symbol.id.clone());
364 self.symbol_buffers
365 .insert(context_symbol.id.clone(), context_symbol.buffer.clone());
366 self.context.push(AssistantContext::Symbol(SymbolContext {
367 id,
368 context_symbol,
369 }));
370 }
371
372 pub fn add_thread(
373 &mut self,
374 thread: Entity<Thread>,
375 remove_if_exists: bool,
376 cx: &mut Context<Self>,
377 ) {
378 if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
379 if remove_if_exists {
380 self.remove_context(context_id);
381 }
382 } else {
383 self.insert_thread(thread, cx);
384 }
385 }
386
387 pub fn wait_for_summaries(&mut self, cx: &App) -> Task<()> {
388 let tasks = std::mem::take(&mut self.thread_summary_tasks);
389
390 cx.spawn(async move |_cx| {
391 join_all(tasks).await;
392 })
393 }
394
395 fn insert_thread(&mut self, thread: Entity<Thread>, cx: &mut App) {
396 if let Some(summary_task) =
397 thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
398 {
399 let thread = thread.clone();
400 let thread_store = self.thread_store.clone();
401
402 self.thread_summary_tasks.push(cx.spawn(async move |cx| {
403 summary_task.await;
404
405 if let Some(thread_store) = thread_store {
406 // Save thread so its summary can be reused later
407 let save_task = thread_store
408 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx));
409
410 if let Some(save_task) = save_task.ok() {
411 save_task.await.log_err();
412 }
413 }
414 }));
415 }
416
417 let id = self.next_context_id.post_inc();
418
419 let text = thread.read(cx).latest_detailed_summary_or_text();
420
421 self.threads.insert(thread.read(cx).id().clone(), id);
422 self.context
423 .push(AssistantContext::Thread(ThreadContext { id, thread, text }));
424 }
425
426 pub fn add_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
427 if self.includes_url(&url).is_none() {
428 self.insert_fetched_url(url, text);
429 }
430 }
431
432 fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
433 let id = self.next_context_id.post_inc();
434
435 self.fetched_urls.insert(url.clone(), id);
436 self.context
437 .push(AssistantContext::FetchedUrl(FetchedUrlContext {
438 id,
439 url: url.into(),
440 text: text.into(),
441 }));
442 }
443
444 pub fn accept_suggested_context(
445 &mut self,
446 suggested: &SuggestedContext,
447 cx: &mut Context<ContextStore>,
448 ) -> Task<Result<()>> {
449 match suggested {
450 SuggestedContext::File {
451 buffer,
452 icon_path: _,
453 name: _,
454 } => {
455 if let Some(buffer) = buffer.upgrade() {
456 return self.add_file_from_buffer(buffer, cx);
457 };
458 }
459 SuggestedContext::Thread { thread, name: _ } => {
460 if let Some(thread) = thread.upgrade() {
461 self.insert_thread(thread, cx);
462 };
463 }
464 }
465 Task::ready(Ok(()))
466 }
467
468 pub fn remove_context(&mut self, id: ContextId) {
469 let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
470 return;
471 };
472
473 match self.context.remove(ix) {
474 AssistantContext::File(_) => {
475 self.files.retain(|_, context_id| *context_id != id);
476 }
477 AssistantContext::Directory(_) => {
478 self.directories.retain(|_, context_id| *context_id != id);
479 }
480 AssistantContext::Symbol(symbol) => {
481 if let Some(symbols_in_path) =
482 self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
483 {
484 symbols_in_path.retain(|s| {
485 self.symbols
486 .get(s)
487 .map_or(false, |context_id| *context_id != id)
488 });
489 }
490 self.symbol_buffers.remove(&symbol.context_symbol.id);
491 self.symbols.retain(|_, context_id| *context_id != id);
492 }
493 AssistantContext::FetchedUrl(_) => {
494 self.fetched_urls.retain(|_, context_id| *context_id != id);
495 }
496 AssistantContext::Thread(_) => {
497 self.threads.retain(|_, context_id| *context_id != id);
498 }
499 }
500 }
501
502 /// Returns whether the buffer is already included directly in the context, or if it will be
503 /// included in the context via a directory. Directory inclusion is based on paths rather than
504 /// buffer IDs as the directory will be re-scanned.
505 pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
506 if let Some(context_id) = self.files.get(&buffer_id) {
507 return Some(FileInclusion::Direct(*context_id));
508 }
509
510 self.will_include_file_path_via_directory(path)
511 }
512
513 /// Returns whether this file path is already included directly in the context, or if it will be
514 /// included in the context via a directory.
515 pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
516 if !self.files.is_empty() {
517 let found_file_context = self.context.iter().find(|context| match &context {
518 AssistantContext::File(file_context) => {
519 let buffer = file_context.context_buffer.buffer.read(cx);
520 if let Some(file_path) = buffer_path_log_err(buffer, cx) {
521 *file_path == *path
522 } else {
523 false
524 }
525 }
526 _ => false,
527 });
528 if let Some(context) = found_file_context {
529 return Some(FileInclusion::Direct(context.id()));
530 }
531 }
532
533 self.will_include_file_path_via_directory(path)
534 }
535
536 fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
537 if self.directories.is_empty() {
538 return None;
539 }
540
541 let mut buf = path.to_path_buf();
542
543 while buf.pop() {
544 if let Some(_) = self.directories.get(&buf) {
545 return Some(FileInclusion::InDirectory(buf));
546 }
547 }
548
549 None
550 }
551
552 pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
553 if let Some(context_id) = self.directories.get(path) {
554 return Some(FileInclusion::Direct(*context_id));
555 }
556
557 self.will_include_file_path_via_directory(path)
558 }
559
560 pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
561 self.symbols.get(symbol_id).copied()
562 }
563
564 pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
565 &self.symbols_by_path
566 }
567
568 pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
569 self.symbol_buffers.get(symbol_id).cloned()
570 }
571
572 pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
573 self.threads.get(thread_id).copied()
574 }
575
576 pub fn includes_url(&self, url: &str) -> Option<ContextId> {
577 self.fetched_urls.get(url).copied()
578 }
579
580 /// Replaces the context that matches the ID of the new context, if any match.
581 fn replace_context(&mut self, new_context: AssistantContext) {
582 let id = new_context.id();
583 for context in self.context.iter_mut() {
584 if context.id() == id {
585 *context = new_context;
586 break;
587 }
588 }
589 }
590
591 pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
592 self.context
593 .iter()
594 .filter_map(|context| match context {
595 AssistantContext::File(file) => {
596 let buffer = file.context_buffer.buffer.read(cx);
597 buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
598 }
599 AssistantContext::Directory(_)
600 | AssistantContext::Symbol(_)
601 | AssistantContext::FetchedUrl(_)
602 | AssistantContext::Thread(_) => None,
603 })
604 .collect()
605 }
606
607 pub fn thread_ids(&self) -> HashSet<ThreadId> {
608 self.threads.keys().cloned().collect()
609 }
610}
611
612pub enum FileInclusion {
613 Direct(ContextId),
614 InDirectory(PathBuf),
615}
616
617// ContextBuffer without text.
618struct BufferInfo {
619 buffer_entity: Entity<Buffer>,
620 file: Arc<dyn File>,
621 id: BufferId,
622 version: clock::Global,
623}
624
625fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
626 ContextBuffer {
627 id: info.id,
628 buffer: info.buffer_entity,
629 file: info.file,
630 version: info.version,
631 text,
632 }
633}
634
635fn make_context_symbol(
636 info: BufferInfo,
637 path: ProjectPath,
638 name: SharedString,
639 range: Range<Anchor>,
640 enclosing_range: Range<Anchor>,
641 text: SharedString,
642) -> ContextSymbol {
643 ContextSymbol {
644 id: ContextSymbolId { name, range, path },
645 buffer_version: info.version,
646 enclosing_range,
647 buffer: info.buffer_entity,
648 text,
649 }
650}
651
652fn collect_buffer_info_and_text(
653 path: Arc<Path>,
654 buffer_entity: Entity<Buffer>,
655 buffer: &Buffer,
656 range: Option<Range<Anchor>>,
657 cx: AsyncApp,
658) -> Result<(BufferInfo, Task<SharedString>)> {
659 let buffer_info = BufferInfo {
660 id: buffer.remote_id(),
661 buffer_entity,
662 file: buffer
663 .file()
664 .context("buffer context must have a file")?
665 .clone(),
666 version: buffer.version(),
667 };
668 // Important to collect version at the same time as content so that staleness logic is correct.
669 let content = if let Some(range) = range {
670 buffer.text_for_range(range).collect::<Rope>()
671 } else {
672 buffer.as_rope().clone()
673 };
674 let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
675 Ok((buffer_info, text_task))
676}
677
678pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
679 if let Some(file) = buffer.file() {
680 let mut path = file.path().clone();
681 if path.as_os_str().is_empty() {
682 path = file.full_path(cx).into();
683 }
684 Some(path)
685 } else {
686 log::error!("Buffer that had a path unexpectedly no longer has a path.");
687 None
688 }
689}
690
691fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
692 let path_extension = path.extension().and_then(|ext| ext.to_str());
693 let path_string = path.to_string_lossy();
694 let capacity = 3
695 + path_extension.map_or(0, |extension| extension.len() + 1)
696 + path_string.len()
697 + 1
698 + content.len()
699 + 5;
700 let mut buffer = String::with_capacity(capacity);
701
702 buffer.push_str("```");
703
704 if let Some(extension) = path_extension {
705 buffer.push_str(extension);
706 buffer.push(' ');
707 }
708 buffer.push_str(&path_string);
709
710 buffer.push('\n');
711 for chunk in content.chunks() {
712 buffer.push_str(&chunk);
713 }
714
715 if !buffer.ends_with('\n') {
716 buffer.push('\n');
717 }
718
719 buffer.push_str("```\n");
720
721 debug_assert!(
722 buffer.len() == capacity - 1 || buffer.len() == capacity,
723 "to_fenced_codeblock calculated capacity of {}, but length was {}",
724 capacity,
725 buffer.len(),
726 );
727
728 buffer.into()
729}
730
731fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
732 let mut files = Vec::new();
733
734 for entry in worktree.child_entries(path) {
735 if entry.is_dir() {
736 files.extend(collect_files_in_path(worktree, &entry.path));
737 } else if entry.is_file() {
738 files.push(entry.path.clone());
739 }
740 }
741
742 files
743}
744
745pub fn refresh_context_store_text(
746 context_store: Entity<ContextStore>,
747 changed_buffers: &HashSet<Entity<Buffer>>,
748 cx: &App,
749) -> impl Future<Output = Vec<ContextId>> + use<> {
750 let mut tasks = Vec::new();
751
752 for context in &context_store.read(cx).context {
753 let id = context.id();
754
755 let task = maybe!({
756 match context {
757 AssistantContext::File(file_context) => {
758 if changed_buffers.is_empty()
759 || changed_buffers.contains(&file_context.context_buffer.buffer)
760 {
761 let context_store = context_store.clone();
762 return refresh_file_text(context_store, file_context, cx);
763 }
764 }
765 AssistantContext::Directory(directory_context) => {
766 let should_refresh = changed_buffers.is_empty()
767 || changed_buffers.iter().any(|buffer| {
768 let buffer = buffer.read(cx);
769
770 buffer_path_log_err(&buffer, cx).map_or(false, |path| {
771 path.starts_with(&directory_context.project_path.path)
772 })
773 });
774
775 if should_refresh {
776 let context_store = context_store.clone();
777 return refresh_directory_text(context_store, directory_context, cx);
778 }
779 }
780 AssistantContext::Symbol(symbol_context) => {
781 if changed_buffers.is_empty()
782 || changed_buffers.contains(&symbol_context.context_symbol.buffer)
783 {
784 let context_store = context_store.clone();
785 return refresh_symbol_text(context_store, symbol_context, cx);
786 }
787 }
788 AssistantContext::Thread(thread_context) => {
789 if changed_buffers.is_empty() {
790 let context_store = context_store.clone();
791 return Some(refresh_thread_text(context_store, thread_context, cx));
792 }
793 }
794 // Intentionally omit refreshing fetched URLs as it doesn't seem all that useful,
795 // and doing the caching properly could be tricky (unless it's already handled by
796 // the HttpClient?).
797 AssistantContext::FetchedUrl(_) => {}
798 }
799
800 None
801 });
802
803 if let Some(task) = task {
804 tasks.push(task.map(move |_| id));
805 }
806 }
807
808 future::join_all(tasks)
809}
810
811fn refresh_file_text(
812 context_store: Entity<ContextStore>,
813 file_context: &FileContext,
814 cx: &App,
815) -> Option<Task<()>> {
816 let id = file_context.id;
817 let task = refresh_context_buffer(&file_context.context_buffer, cx);
818 if let Some(task) = task {
819 Some(cx.spawn(async move |cx| {
820 let context_buffer = task.await;
821 context_store
822 .update(cx, |context_store, _| {
823 let new_file_context = FileContext { id, context_buffer };
824 context_store.replace_context(AssistantContext::File(new_file_context));
825 })
826 .ok();
827 }))
828 } else {
829 None
830 }
831}
832
833fn refresh_directory_text(
834 context_store: Entity<ContextStore>,
835 directory_context: &DirectoryContext,
836 cx: &App,
837) -> Option<Task<()>> {
838 let mut stale = false;
839 let futures = directory_context
840 .context_buffers
841 .iter()
842 .map(|context_buffer| {
843 if let Some(refresh_task) = refresh_context_buffer(context_buffer, cx) {
844 stale = true;
845 future::Either::Left(refresh_task)
846 } else {
847 future::Either::Right(future::ready((*context_buffer).clone()))
848 }
849 })
850 .collect::<Vec<_>>();
851
852 if !stale {
853 return None;
854 }
855
856 let context_buffers = future::join_all(futures);
857
858 let id = directory_context.id;
859 let project_path = directory_context.project_path.clone();
860 Some(cx.spawn(async move |cx| {
861 let context_buffers = context_buffers.await;
862 context_store
863 .update(cx, |context_store, _| {
864 let new_directory_context = DirectoryContext {
865 id,
866 project_path,
867 context_buffers,
868 };
869 context_store.replace_context(AssistantContext::Directory(new_directory_context));
870 })
871 .ok();
872 }))
873}
874
875fn refresh_symbol_text(
876 context_store: Entity<ContextStore>,
877 symbol_context: &SymbolContext,
878 cx: &App,
879) -> Option<Task<()>> {
880 let id = symbol_context.id;
881 let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
882 if let Some(task) = task {
883 Some(cx.spawn(async move |cx| {
884 let context_symbol = task.await;
885 context_store
886 .update(cx, |context_store, _| {
887 let new_symbol_context = SymbolContext { id, context_symbol };
888 context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
889 })
890 .ok();
891 }))
892 } else {
893 None
894 }
895}
896
897fn refresh_thread_text(
898 context_store: Entity<ContextStore>,
899 thread_context: &ThreadContext,
900 cx: &App,
901) -> Task<()> {
902 let id = thread_context.id;
903 let thread = thread_context.thread.clone();
904 cx.spawn(async move |cx| {
905 context_store
906 .update(cx, |context_store, cx| {
907 let text = thread.read(cx).latest_detailed_summary_or_text();
908 context_store.replace_context(AssistantContext::Thread(ThreadContext {
909 id,
910 thread,
911 text,
912 }));
913 })
914 .ok();
915 })
916}
917
918fn refresh_context_buffer(
919 context_buffer: &ContextBuffer,
920 cx: &App,
921) -> Option<impl Future<Output = ContextBuffer> + use<>> {
922 let buffer = context_buffer.buffer.read(cx);
923 let path = buffer_path_log_err(buffer, cx)?;
924 if buffer.version.changed_since(&context_buffer.version) {
925 let (buffer_info, text_task) = collect_buffer_info_and_text(
926 path,
927 context_buffer.buffer.clone(),
928 buffer,
929 None,
930 cx.to_async(),
931 )
932 .log_err()?;
933 Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
934 } else {
935 None
936 }
937}
938
939fn refresh_context_symbol(
940 context_symbol: &ContextSymbol,
941 cx: &App,
942) -> Option<impl Future<Output = ContextSymbol> + use<>> {
943 let buffer = context_symbol.buffer.read(cx);
944 let path = buffer_path_log_err(buffer, cx)?;
945 let project_path = buffer.project_path(cx)?;
946 if buffer.version.changed_since(&context_symbol.buffer_version) {
947 let (buffer_info, text_task) = collect_buffer_info_and_text(
948 path,
949 context_symbol.buffer.clone(),
950 buffer,
951 Some(context_symbol.enclosing_range.clone()),
952 cx.to_async(),
953 )
954 .log_err()?;
955 let name = context_symbol.id.name.clone();
956 let range = context_symbol.id.range.clone();
957 let enclosing_range = context_symbol.enclosing_range.clone();
958 Some(text_task.map(move |text| {
959 make_context_symbol(
960 buffer_info,
961 project_path,
962 name,
963 range,
964 enclosing_range,
965 text,
966 )
967 }))
968 } else {
969 None
970 }
971}