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