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