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