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 Some(file_path(file, cx))
637 } else {
638 log::error!("Buffer that had a path unexpectedly no longer has a path.");
639 None
640 }
641}
642
643pub fn file_path(file: &Arc<dyn File>, cx: &App) -> Arc<Path> {
644 let mut path = file.path().clone();
645 if path.as_os_str().is_empty() {
646 path = file.full_path(cx).into();
647 }
648 return path;
649}
650
651fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
652 let path_extension = path.extension().and_then(|ext| ext.to_str());
653 let path_string = path.to_string_lossy();
654 let capacity = 3
655 + path_extension.map_or(0, |extension| extension.len() + 1)
656 + path_string.len()
657 + 1
658 + content.len()
659 + 5;
660 let mut buffer = String::with_capacity(capacity);
661
662 buffer.push_str("```");
663
664 if let Some(extension) = path_extension {
665 buffer.push_str(extension);
666 buffer.push(' ');
667 }
668 buffer.push_str(&path_string);
669
670 buffer.push('\n');
671 for chunk in content.chunks() {
672 buffer.push_str(&chunk);
673 }
674
675 if !buffer.ends_with('\n') {
676 buffer.push('\n');
677 }
678
679 buffer.push_str("```\n");
680
681 debug_assert!(
682 buffer.len() == capacity - 1 || buffer.len() == capacity,
683 "to_fenced_codeblock calculated capacity of {}, but length was {}",
684 capacity,
685 buffer.len(),
686 );
687
688 buffer.into()
689}
690
691fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
692 let mut files = Vec::new();
693
694 for entry in worktree.child_entries(path) {
695 if entry.is_dir() {
696 files.extend(collect_files_in_path(worktree, &entry.path));
697 } else if entry.is_file() {
698 files.push(entry.path.clone());
699 }
700 }
701
702 files
703}
704
705pub fn refresh_context_store_text(
706 context_store: Entity<ContextStore>,
707 changed_buffers: &HashSet<Entity<Buffer>>,
708 cx: &App,
709) -> impl Future<Output = Vec<ContextId>> + use<> {
710 let mut tasks = Vec::new();
711
712 for context in &context_store.read(cx).context {
713 let id = context.id();
714
715 let task = maybe!({
716 match context {
717 AssistantContext::File(file_context) => {
718 if changed_buffers.is_empty()
719 || changed_buffers.contains(&file_context.context_buffer.buffer)
720 {
721 let context_store = context_store.clone();
722 return refresh_file_text(context_store, file_context, cx);
723 }
724 }
725 AssistantContext::Directory(directory_context) => {
726 let should_refresh = changed_buffers.is_empty()
727 || changed_buffers.iter().any(|buffer| {
728 let buffer = buffer.read(cx);
729
730 buffer_path_log_err(&buffer, cx).map_or(false, |path| {
731 path.starts_with(&directory_context.project_path.path)
732 })
733 });
734
735 if should_refresh {
736 let context_store = context_store.clone();
737 return refresh_directory_text(context_store, directory_context, cx);
738 }
739 }
740 AssistantContext::Symbol(symbol_context) => {
741 if changed_buffers.is_empty()
742 || changed_buffers.contains(&symbol_context.context_symbol.buffer)
743 {
744 let context_store = context_store.clone();
745 return refresh_symbol_text(context_store, symbol_context, cx);
746 }
747 }
748 AssistantContext::Thread(thread_context) => {
749 if changed_buffers.is_empty() {
750 let context_store = context_store.clone();
751 return Some(refresh_thread_text(context_store, thread_context, cx));
752 }
753 }
754 // Intentionally omit refreshing fetched URLs as it doesn't seem all that useful,
755 // and doing the caching properly could be tricky (unless it's already handled by
756 // the HttpClient?).
757 AssistantContext::FetchedUrl(_) => {}
758 }
759
760 None
761 });
762
763 if let Some(task) = task {
764 tasks.push(task.map(move |_| id));
765 }
766 }
767
768 future::join_all(tasks)
769}
770
771fn refresh_file_text(
772 context_store: Entity<ContextStore>,
773 file_context: &FileContext,
774 cx: &App,
775) -> Option<Task<()>> {
776 let id = file_context.id;
777 let task = refresh_context_buffer(&file_context.context_buffer, cx);
778 if let Some(task) = task {
779 Some(cx.spawn(async move |cx| {
780 let context_buffer = task.await;
781 context_store
782 .update(cx, |context_store, _| {
783 let new_file_context = FileContext { id, context_buffer };
784 context_store.replace_context(AssistantContext::File(new_file_context));
785 })
786 .ok();
787 }))
788 } else {
789 None
790 }
791}
792
793fn refresh_directory_text(
794 context_store: Entity<ContextStore>,
795 directory_context: &DirectoryContext,
796 cx: &App,
797) -> Option<Task<()>> {
798 let mut stale = false;
799 let futures = directory_context
800 .context_buffers
801 .iter()
802 .map(|context_buffer| {
803 if let Some(refresh_task) = refresh_context_buffer(context_buffer, cx) {
804 stale = true;
805 future::Either::Left(refresh_task)
806 } else {
807 future::Either::Right(future::ready((*context_buffer).clone()))
808 }
809 })
810 .collect::<Vec<_>>();
811
812 if !stale {
813 return None;
814 }
815
816 let context_buffers = future::join_all(futures);
817
818 let id = directory_context.id;
819 let project_path = directory_context.project_path.clone();
820 Some(cx.spawn(async move |cx| {
821 let context_buffers = context_buffers.await;
822 context_store
823 .update(cx, |context_store, _| {
824 let new_directory_context = DirectoryContext {
825 id,
826 project_path,
827 context_buffers,
828 };
829 context_store.replace_context(AssistantContext::Directory(new_directory_context));
830 })
831 .ok();
832 }))
833}
834
835fn refresh_symbol_text(
836 context_store: Entity<ContextStore>,
837 symbol_context: &SymbolContext,
838 cx: &App,
839) -> Option<Task<()>> {
840 let id = symbol_context.id;
841 let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
842 if let Some(task) = task {
843 Some(cx.spawn(async move |cx| {
844 let context_symbol = task.await;
845 context_store
846 .update(cx, |context_store, _| {
847 let new_symbol_context = SymbolContext { id, context_symbol };
848 context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
849 })
850 .ok();
851 }))
852 } else {
853 None
854 }
855}
856
857fn refresh_thread_text(
858 context_store: Entity<ContextStore>,
859 thread_context: &ThreadContext,
860 cx: &App,
861) -> Task<()> {
862 let id = thread_context.id;
863 let thread = thread_context.thread.clone();
864 cx.spawn(async move |cx| {
865 context_store
866 .update(cx, |context_store, cx| {
867 let text = thread.read(cx).text().into();
868 context_store.replace_context(AssistantContext::Thread(ThreadContext {
869 id,
870 thread,
871 text,
872 }));
873 })
874 .ok();
875 })
876}
877
878fn refresh_context_buffer(
879 context_buffer: &ContextBuffer,
880 cx: &App,
881) -> Option<impl Future<Output = ContextBuffer> + use<>> {
882 let buffer = context_buffer.buffer.read(cx);
883 let path = buffer_path_log_err(buffer, cx)?;
884 if buffer.version.changed_since(&context_buffer.version) {
885 let (buffer_info, text_task) = collect_buffer_info_and_text(
886 path,
887 context_buffer.buffer.clone(),
888 buffer,
889 None,
890 cx.to_async(),
891 )
892 .log_err()?;
893 Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
894 } else {
895 None
896 }
897}
898
899fn refresh_context_symbol(
900 context_symbol: &ContextSymbol,
901 cx: &App,
902) -> Option<impl Future<Output = ContextSymbol> + use<>> {
903 let buffer = context_symbol.buffer.read(cx);
904 let path = buffer_path_log_err(buffer, cx)?;
905 let project_path = buffer.project_path(cx)?;
906 if buffer.version.changed_since(&context_symbol.buffer_version) {
907 let (buffer_info, text_task) = collect_buffer_info_and_text(
908 path,
909 context_symbol.buffer.clone(),
910 buffer,
911 Some(context_symbol.enclosing_range.clone()),
912 cx.to_async(),
913 )
914 .log_err()?;
915 let name = context_symbol.id.name.clone();
916 let range = context_symbol.id.range.clone();
917 let enclosing_range = context_symbol.enclosing_range.clone();
918 Some(text_task.map(move |text| {
919 make_context_symbol(
920 buffer_info,
921 project_path,
922 name,
923 range,
924 enclosing_range,
925 text,
926 )
927 }))
928 } else {
929 None
930 }
931}