History

Fix directory context paths (#28459)

Release Notes: - N/A

Michael Sloan committed 1 year ago

Commit ba767a11 parent 23c3f5f9 files · +135 −99Browse files

Unified · Split

9 files
  1. crates/agent/src/active_thread.rs +2 −2
  2. crates/agent/src/context.rs +18 −9
  3. crates/agent/src/context_picker.rs +3 −1
  4. crates/agent/src/context_picker/file_context_picker.rs +16 −7
  5. crates/agent/src/context_store.rs +55 −48
  6. crates/agent/src/context_strip.rs +7 −8
  7. crates/agent/src/ui/context_pill.rs +4 −3
  8. crates/project/src/project.rs +4 −0
  9. crates/worktree/src/worktree.rs +26 −21
crates/agent/src/active_thread.rs+2−2
@@ -2840,10 +2840,10 @@ pub(crate) fn open_context( · 2839 unmodified lines
2840 }2840 }
2841 }2841 }
2842 AssistantContext::Directory(directory_context) => {2842 AssistantContext::Directory(directory_context) => {
2843 let path = directory_context.project_path.clone();2843 let project_path = directory_context.project_path(cx);
2844 workspace.update(cx, |workspace, cx| {2844 workspace.update(cx, |workspace, cx| {
2845 workspace.project().update(cx, |project, cx| {2845 workspace.project().update(cx, |project, cx| {
2846 if let Some(entry) = project.entry_for_path(&path, cx) {2846 if let Some(entry) = project.entry_for_path(&project_path, cx) {
2847 cx.emit(project::Event::RevealInProjectPanel(entry.id));2847 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2848 }2848 }
2849 })2849 })
crates/agent/src/context.rs+18−9
@@ -1,9 +1,9 @@
1use std::{ops::Range, sync::Arc};1use std::{ops::Range, path::Path, sync::Arc};
2 2
3use gpui::{App, Entity, SharedString};3use gpui::{App, Entity, SharedString};
4use language::{Buffer, File};4use language::{Buffer, File};
5use language_model::LanguageModelRequestMessage;5use language_model::LanguageModelRequestMessage;
6use project::ProjectPath;6use project::{ProjectPath, Worktree};
7use serde::{Deserialize, Serialize};7use serde::{Deserialize, Serialize};
8use text::{Anchor, BufferId};8use text::{Anchor, BufferId};
9use ui::IconName;9use ui::IconName;
@@ -69,10 +69,21 @@ pub struct FileContext { · 59 unmodified lines
69#[derive(Debug, Clone)]69#[derive(Debug, Clone)]
70pub struct DirectoryContext {70pub struct DirectoryContext {
71 pub id: ContextId,71 pub id: ContextId,
72 pub project_path: ProjectPath,72 pub worktree: Entity<Worktree>,
73 pub path: Arc<Path>,
74 /// Buffers of the files within the directory.
73 pub context_buffers: Vec<ContextBuffer>,75 pub context_buffers: Vec<ContextBuffer>,
74}76}
75 77
78impl DirectoryContext {
79 pub fn project_path(&self, cx: &App) -> ProjectPath {
80 ProjectPath {
81 worktree_id: self.worktree.read(cx).id(),
82 path: self.path.clone(),
83 }
84 }
85}
86
76#[derive(Debug, Clone)]87#[derive(Debug, Clone)]
77pub struct SymbolContext {88pub struct SymbolContext {
78 pub id: ContextId,89 pub id: ContextId,
@@ -86,12 +97,11 @@ pub struct FetchedUrlContext { · 7 unmodified lines
86 pub text: SharedString,97 pub text: SharedString,
87}98}
88 99
89// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
90// explicitly or have a WeakModel<Thread> and remove during snapshot.
91
92#[derive(Debug, Clone)]100#[derive(Debug, Clone)]
93pub struct ThreadContext {101pub struct ThreadContext {
94 pub id: ContextId,102 pub id: ContextId,
103 // TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
104 // a WeakEntity and handle removal from the UI when it has dropped.
95 pub thread: Entity<Thread>,105 pub thread: Entity<Thread>,
96 pub text: SharedString,106 pub text: SharedString,
97}107}
@@ -105,12 +115,11 @@ impl ThreadContext { · 7 unmodified lines
105 }115 }
106}116}
107 117
108// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
109// the context from the message editor in this case.
110
111#[derive(Clone)]118#[derive(Clone)]
112pub struct ContextBuffer {119pub struct ContextBuffer {
113 pub id: BufferId,120 pub id: BufferId,
121 // TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
122 // a WeakEntity and handle removal from the UI when it has dropped.
114 pub buffer: Entity<Buffer>,123 pub buffer: Entity<Buffer>,
115 pub file: Arc<dyn File>,124 pub file: Arc<dyn File>,
116 pub version: clock::Global,125 pub version: clock::Global,
crates/agent/src/context_picker.rs+3−1
@@ -289,12 +289,14 @@ impl ContextPicker { · 288 unmodified lines
289 path_prefix,289 path_prefix,
290 } => {290 } => {
291 let context_store = self.context_store.clone();291 let context_store = self.context_store.clone();
292 let worktree_id = project_path.worktree_id;
292 let path = project_path.path.clone();293 let path = project_path.path.clone();
293 294
294 ContextMenuItem::custom_entry(295 ContextMenuItem::custom_entry(
295 move |_window, cx| {296 move |_window, cx| {
296 render_file_context_entry(297 render_file_context_entry(
297 ElementId::NamedInteger("ctx-recent".into(), ix),298 ElementId::NamedInteger("ctx-recent".into(), ix),
299 worktree_id,
298 &path,300 &path,
299 &path_prefix,301 &path_prefix,
300 false,302 false,
@@ -466,7 +468,7 @@ fn recent_context_picker_entries( · 165 unmodified lines
466 recent.extend(468 recent.extend(
467 workspace469 workspace
468 .recent_navigation_history_iter(cx)470 .recent_navigation_history_iter(cx)
469 .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))471 .filter(|(path, _)| !current_files.contains(path))
470 .take(4)472 .take(4)
471 .filter_map(|(project_path, _)| {473 .filter_map(|(project_path, _)| {
472 project474 project
crates/agent/src/context_picker/file_context_picker.rs+16−7
@@ -189,6 +189,7 @@ impl PickerDelegate for FileContextPickerDelegate { · 188 unmodified lines
189 .toggle_state(selected)189 .toggle_state(selected)
190 .child(render_file_context_entry(190 .child(render_file_context_entry(
191 ElementId::NamedInteger("file-ctx-picker".into(), ix),191 ElementId::NamedInteger("file-ctx-picker".into(), ix),
192 WorktreeId::from_usize(mat.worktree_id),
192 &mat.path,193 &mat.path,
193 &mat.path_prefix,194 &mat.path_prefix,
194 mat.is_dir,195 mat.is_dir,
@@ -328,19 +329,26 @@ pub fn extract_file_name_and_directory( · 133 unmodified lines
328 329
329pub fn render_file_context_entry(330pub fn render_file_context_entry(
330 id: ElementId,331 id: ElementId,
331 path: &Path,332 worktree_id: WorktreeId,
333 path: &Arc<Path>,
332 path_prefix: &Arc<str>,334 path_prefix: &Arc<str>,
333 is_directory: bool,335 is_directory: bool,
334 context_store: WeakEntity<ContextStore>,336 context_store: WeakEntity<ContextStore>,
335 cx: &App,337 cx: &App,
336) -> Stateful<Div> {338) -> Stateful<Div> {
337 let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);339 let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
338 340
339 let added = context_store.upgrade().and_then(|context_store| {341 let added = context_store.upgrade().and_then(|context_store| {
342 let project_path = ProjectPath {
343 worktree_id,
344 path: path.clone(),
345 };
340 if is_directory {346 if is_directory {
341 context_store.read(cx).includes_directory(path)347 context_store.read(cx).includes_directory(&project_path)
342 } else {348 } else {
343 context_store.read(cx).will_include_file_path(path, cx)349 context_store
350 .read(cx)
351 .will_include_file_path(&project_path, cx)
344 }352 }
345 });353 });
346 354
@@ -380,8 +388,9 @@ pub fn render_file_context_entry( · 33 unmodified lines
380 )388 )
381 .child(Label::new("Added").size(LabelSize::Small)),389 .child(Label::new("Added").size(LabelSize::Small)),
382 ),390 ),
383 FileInclusion::InDirectory(dir_name) => {391 FileInclusion::InDirectory(directory_project_path) => {
384 let dir_name = dir_name.to_string_lossy().into_owned();392 // TODO: Consider using worktree full_path to include worktree name.
393 let directory_path = directory_project_path.path.to_string_lossy().into_owned();
385 394
386 el.child(395 el.child(
387 h_flex()396 h_flex()
@@ -395,7 +404,7 @@ pub fn render_file_context_entry( · 7 unmodified lines
395 )404 )
396 .child(Label::new("Included").size(LabelSize::Small)),405 .child(Label::new("Included").size(LabelSize::Small)),
397 )406 )
398 .tooltip(Tooltip::text(format!("in {dir_name}")))407 .tooltip(Tooltip::text(format!("in {directory_path}")))
399 }408 }
400 })409 })
401}410}
crates/agent/src/context_store.rs+55−48
@@ -1,5 +1,5 @@
1use std::ops::Range;1use std::ops::Range;
2use std::path::{Path, PathBuf};2use std::path::Path;
3use std::sync::Arc;3use std::sync::Arc;
4 4
5use anyhow::{Context as _, Result, anyhow};5use anyhow::{Context as _, Result, anyhow};
@@ -28,7 +28,7 @@ pub struct ContextStore { · 22 unmodified lines
28 // TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.28 // TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
29 next_context_id: ContextId,29 next_context_id: ContextId,
30 files: BTreeMap<BufferId, ContextId>,30 files: BTreeMap<BufferId, ContextId>,
31 directories: HashMap<PathBuf, ContextId>,31 directories: HashMap<ProjectPath, ContextId>,
32 symbols: HashMap<ContextSymbolId, ContextId>,32 symbols: HashMap<ContextSymbolId, ContextId>,
33 symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,33 symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
34 symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,34 symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
@@ -93,7 +93,7 @@ impl ContextStore { · 58 unmodified lines
93 let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;93 let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
94 94
95 let already_included = this.update(cx, |this, cx| {95 let already_included = this.update(cx, |this, cx| {
96 match this.will_include_buffer(buffer_id, &project_path.path) {96 match this.will_include_buffer(buffer_id, &project_path) {
97 Some(FileInclusion::Direct(context_id)) => {97 Some(FileInclusion::Direct(context_id)) => {
98 if remove_if_exists {98 if remove_if_exists {
99 this.remove_context(context_id, cx);99 this.remove_context(context_id, cx);
@@ -159,7 +159,7 @@ impl ContextStore { · 59 unmodified lines
159 return Task::ready(Err(anyhow!("failed to read project")));159 return Task::ready(Err(anyhow!("failed to read project")));
160 };160 };
161 161
162 let already_included = match self.includes_directory(&project_path.path) {162 let already_included = match self.includes_directory(&project_path) {
163 Some(FileInclusion::Direct(context_id)) => {163 Some(FileInclusion::Direct(context_id)) => {
164 if remove_if_exists {164 if remove_if_exists {
165 self.remove_context(context_id, cx);165 self.remove_context(context_id, cx);
@@ -223,14 +223,12 @@ impl ContextStore { · 57 unmodified lines
223 .collect::<Vec<_>>();223 .collect::<Vec<_>>();
224 224
225 if context_buffers.is_empty() {225 if context_buffers.is_empty() {
226 return Err(anyhow!(226 let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
227 "No text files found in {}",227 return Err(anyhow!("No text files found in {}", &full_path.display()));
228 &project_path.path.display()
229 ));
230 }228 }
231 229
232 this.update(cx, |this, cx| {230 this.update(cx, |this, cx| {
233 this.insert_directory(project_path, context_buffers, cx);231 this.insert_directory(worktree, project_path, context_buffers, cx);
234 })?;232 })?;
235 233
236 anyhow::Ok(())234 anyhow::Ok(())
@@ -239,17 +237,20 @@ impl ContextStore { · 2 unmodified lines
239 237
240 fn insert_directory(238 fn insert_directory(
241 &mut self,239 &mut self,
240 worktree: Entity<Worktree>,
242 project_path: ProjectPath,241 project_path: ProjectPath,
243 context_buffers: Vec<ContextBuffer>,242 context_buffers: Vec<ContextBuffer>,
244 cx: &mut Context<Self>,243 cx: &mut Context<Self>,
245 ) {244 ) {
246 let id = self.next_context_id.post_inc();245 let id = self.next_context_id.post_inc();
247 self.directories.insert(project_path.path.to_path_buf(), id);246 let path = project_path.path.clone();
247 self.directories.insert(project_path, id);
248 248
249 self.context249 self.context
250 .push(AssistantContext::Directory(DirectoryContext {250 .push(AssistantContext::Directory(DirectoryContext {
251 id,251 id,
252 project_path,252 worktree,
253 path,
253 context_buffers,254 context_buffers,
254 }));255 }));
255 cx.notify();256 cx.notify();
@@ -478,23 +479,31 @@ impl ContextStore { · 222 unmodified lines
478 /// Returns whether the buffer is already included directly in the context, or if it will be479 /// Returns whether the buffer is already included directly in the context, or if it will be
479 /// included in the context via a directory. Directory inclusion is based on paths rather than480 /// included in the context via a directory. Directory inclusion is based on paths rather than
480 /// buffer IDs as the directory will be re-scanned.481 /// buffer IDs as the directory will be re-scanned.
481 pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {482 pub fn will_include_buffer(
483 &self,
484 buffer_id: BufferId,
485 project_path: &ProjectPath,
486 ) -> Option<FileInclusion> {
482 if let Some(context_id) = self.files.get(&buffer_id) {487 if let Some(context_id) = self.files.get(&buffer_id) {
483 return Some(FileInclusion::Direct(*context_id));488 return Some(FileInclusion::Direct(*context_id));
484 }489 }
485 490
486 self.will_include_file_path_via_directory(path)491 self.will_include_file_path_via_directory(project_path)
487 }492 }
488 493
489 /// Returns whether this file path is already included directly in the context, or if it will be494 /// Returns whether this file path is already included directly in the context, or if it will be
490 /// included in the context via a directory.495 /// included in the context via a directory.
491 pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {496 pub fn will_include_file_path(
497 &self,
498 project_path: &ProjectPath,
499 cx: &App,
500 ) -> Option<FileInclusion> {
492 if !self.files.is_empty() {501 if !self.files.is_empty() {
493 let found_file_context = self.context.iter().find(|context| match &context {502 let found_file_context = self.context.iter().find(|context| match &context {
494 AssistantContext::File(file_context) => {503 AssistantContext::File(file_context) => {
495 let buffer = file_context.context_buffer.buffer.read(cx);504 let buffer = file_context.context_buffer.buffer.read(cx);
496 if let Some(file_path) = buffer_path_log_err(buffer, cx) {505 if let Some(context_path) = buffer.project_path(cx) {
497 *file_path == *path506 &context_path == project_path
498 } else {507 } else {
499 false508 false
500 }509 }
@@ -506,31 +515,40 @@ impl ContextStore { · 5 unmodified lines
506 }515 }
507 }516 }
508 517
509 self.will_include_file_path_via_directory(path)518 self.will_include_file_path_via_directory(project_path)
510 }519 }
511 520
512 fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {521 fn will_include_file_path_via_directory(
522 &self,
523 project_path: &ProjectPath,
524 ) -> Option<FileInclusion> {
513 if self.directories.is_empty() {525 if self.directories.is_empty() {
514 return None;526 return None;
515 }527 }
516 528
517 let mut buf = path.to_path_buf();529 let mut path_buf = project_path.path.to_path_buf();
518 530
519 while buf.pop() {531 while path_buf.pop() {
520 if let Some(_) = self.directories.get(&buf) {532 // TODO: This isn't very efficient. Consider using a better representation of the
521 return Some(FileInclusion::InDirectory(buf));533 // directories map.
534 let directory_project_path = ProjectPath {
535 worktree_id: project_path.worktree_id,
536 path: path_buf.clone().into(),
537 };
538 if let Some(_) = self.directories.get(&directory_project_path) {
539 return Some(FileInclusion::InDirectory(directory_project_path));
522 }540 }
523 }541 }
524 542
525 None543 None
526 }544 }
527 545
528 pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {546 pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
529 if let Some(context_id) = self.directories.get(path) {547 if let Some(context_id) = self.directories.get(project_path) {
530 return Some(FileInclusion::Direct(*context_id));548 return Some(FileInclusion::Direct(*context_id));
531 }549 }
532 550
533 self.will_include_file_path_via_directory(path)551 self.will_include_file_path_via_directory(project_path)
534 }552 }
535 553
536 pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {554 pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
@@ -564,13 +582,13 @@ impl ContextStore { · 27 unmodified lines
564 }582 }
565 }583 }
566 584
567 pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {585 pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
568 self.context586 self.context
569 .iter()587 .iter()
570 .filter_map(|context| match context {588 .filter_map(|context| match context {
571 AssistantContext::File(file) => {589 AssistantContext::File(file) => {
572 let buffer = file.context_buffer.buffer.read(cx);590 let buffer = file.context_buffer.buffer.read(cx);
573 buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())591 buffer.project_path(cx)
574 }592 }
575 AssistantContext::Directory(_)593 AssistantContext::Directory(_)
576 | AssistantContext::Symbol(_)594 | AssistantContext::Symbol(_)
@@ -587,7 +605,7 @@ impl ContextStore { · 10 unmodified lines
587 605
588pub enum FileInclusion {606pub enum FileInclusion {
589 Direct(ContextId),607 Direct(ContextId),
590 InDirectory(PathBuf),608 InDirectory(ProjectPath),
591}609}
592 610
593// ContextBuffer without text.611// ContextBuffer without text.
@@ -654,19 +672,6 @@ fn collect_buffer_info_and_text( · 60 unmodified lines
654 Ok((buffer_info, text_task))672 Ok((buffer_info, text_task))
655}673}
656 674
657pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
658 if let Some(file) = buffer.file() {
659 let mut path = file.path().clone();
660 if path.as_os_str().is_empty() {
661 path = file.full_path(cx).into();
662 }
663 Some(path)
664 } else {
665 log::error!("Buffer that had a path unexpectedly no longer has a path.");
666 None
667 }
668}
669
670fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {675fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
671 let path_extension = path.extension().and_then(|ext| ext.to_str());676 let path_extension = path.extension().and_then(|ext| ext.to_str());
672 let path_string = path.to_string_lossy();677 let path_string = path.to_string_lossy();
@@ -742,13 +747,13 @@ pub fn refresh_context_store_text( · 69 unmodified lines
742 }747 }
743 }748 }
744 AssistantContext::Directory(directory_context) => {749 AssistantContext::Directory(directory_context) => {
750 let directory_path = directory_context.project_path(cx);
745 let should_refresh = changed_buffers.is_empty()751 let should_refresh = changed_buffers.is_empty()
746 || changed_buffers.iter().any(|buffer| {752 || changed_buffers.iter().any(|buffer| {
747 let buffer = buffer.read(cx);753 let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
748 754 return false;
749 buffer_path_log_err(&buffer, cx).map_or(false, |path| {755 };
750 path.starts_with(&directory_context.project_path.path)756 buffer_path.starts_with(&directory_path)
751 })
752 });757 });
753 758
754 if should_refresh {759 if should_refresh {
@@ -835,14 +840,16 @@ fn refresh_directory_text( · 80 unmodified lines
835 let context_buffers = future::join_all(futures);840 let context_buffers = future::join_all(futures);
836 841
837 let id = directory_context.id;842 let id = directory_context.id;
838 let project_path = directory_context.project_path.clone();843 let worktree = directory_context.worktree.clone();
844 let path = directory_context.path.clone();
839 Some(cx.spawn(async move |cx| {845 Some(cx.spawn(async move |cx| {
840 let context_buffers = context_buffers.await;846 let context_buffers = context_buffers.await;
841 context_store847 context_store
842 .update(cx, |context_store, _| {848 .update(cx, |context_store, _| {
843 let new_directory_context = DirectoryContext {849 let new_directory_context = DirectoryContext {
844 id,850 id,
845 project_path,851 worktree,
852 path,
846 context_buffers,853 context_buffers,
847 };854 };
848 context_store.replace_context(AssistantContext::Directory(new_directory_context));855 context_store.replace_context(AssistantContext::Directory(new_directory_context));
crates/agent/src/context_strip.rs+7−8
@@ -1,3 +1,4 @@
1use std::path::Path;
1use std::rc::Rc;2use std::rc::Rc;
2 3
3use collections::HashSet;4use collections::HashSet;
@@ -9,6 +10,7 @@ use gpui::{ · 5 unmodified lines
9};10};
10use itertools::Itertools;11use itertools::Itertools;
11use language::Buffer;12use language::Buffer;
13use project::ProjectItem;
12use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};14use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
13use workspace::{Workspace, notifications::NotifyResultExt};15use workspace::{Workspace, notifications::NotifyResultExt};
14 16
@@ -93,26 +95,23 @@ impl ContextStrip { · 78 unmodified lines
93 let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;95 let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
94 let active_buffer = active_buffer_entity.read(cx);96 let active_buffer = active_buffer_entity.read(cx);
95 97
96 let path = active_buffer.file()?.full_path(cx);98 let project_path = active_buffer.project_path(cx)?;
97 99
98 if self100 if self
99 .context_store101 .context_store
100 .read(cx)102 .read(cx)
101 .will_include_buffer(active_buffer.remote_id(), &path)103 .will_include_buffer(active_buffer.remote_id(), &project_path)
102 .is_some()104 .is_some()
103 {105 {
104 return None;106 return None;
105 }107 }
106 108
107 let name = match path.file_name() {109 let file_name = active_buffer.file()?.file_name(cx);
108 Some(name) => name.to_string_lossy().into_owned().into(),
109 None => path.to_string_lossy().into_owned().into(),
110 };
111 110
112 let icon_path = FileIcons::get_icon(&path, cx);111 let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
113 112
114 Some(SuggestedContext::File {113 Some(SuggestedContext::File {
115 name,114 name: file_name.to_string_lossy().into_owned().into(),
116 buffer: active_buffer_entity.downgrade(),115 buffer: active_buffer_entity.downgrade(),
117 icon_path,116 icon_path,
118 })117 })
crates/agent/src/ui/context_pill.rs+4−3
@@ -280,9 +280,10 @@ impl AddedContext { · 279 unmodified lines
280 }280 }
281 281
282 AssistantContext::Directory(directory_context) => {282 AssistantContext::Directory(directory_context) => {
283 // TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also283 let full_path = directory_context
284 // handle renames?284 .worktree
285 let full_path = &directory_context.project_path.path;285 .read(cx)
286 .full_path(&directory_context.path);
286 let full_path_string: SharedString =287 let full_path_string: SharedString =
287 full_path.to_string_lossy().into_owned().into();288 full_path.to_string_lossy().into_owned().into();
288 let name = full_path289 let name = full_path
crates/project/src/project.rs+4−0
@@ -334,6 +334,10 @@ impl ProjectPath { · 333 unmodified lines
334 path: Path::new("").into(),334 path: Path::new("").into(),
335 }335 }
336 }336 }
337
338 pub fn starts_with(&self, other: &ProjectPath) -> bool {
339 self.worktree_id == other.worktree_id && self.path.starts_with(&other.path)
340 }
337}341}
338 342
339#[derive(Debug, Default)]343#[derive(Debug, Default)]
crates/worktree/src/worktree.rs+26−21
@@ -1172,6 +1172,31 @@ impl Worktree { · 1171 unmodified lines
1172 pub fn is_single_file(&self) -> bool {1172 pub fn is_single_file(&self) -> bool {
1173 self.root_dir().is_none()1173 self.root_dir().is_none()
1174 }1174 }
1175
1176 /// For visible worktrees, returns the path with the worktree name as the first component.
1177 /// Otherwise, returns an absolute path.
1178 pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
1179 let mut full_path = PathBuf::new();
1180
1181 if self.is_visible() {
1182 full_path.push(self.root_name());
1183 } else {
1184 let path = self.abs_path();
1185
1186 if self.is_local() && path.starts_with(home_dir().as_path()) {
1187 full_path.push("~");
1188 full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
1189 } else {
1190 full_path.push(path)
1191 }
1192 }
1193
1194 if worktree_relative_path.components().next().is_some() {
1195 full_path.push(&worktree_relative_path);
1196 }
1197
1198 full_path
1199 }
1175}1200}
1176 1201
1177impl LocalWorktree {1202impl LocalWorktree {
@@ -3229,27 +3254,7 @@ impl language::File for File { · 2051 unmodified lines
3229 }3254 }
3230 3255
3231 fn full_path(&self, cx: &App) -> PathBuf {3256 fn full_path(&self, cx: &App) -> PathBuf {
3232 let mut full_path = PathBuf::new();3257 self.worktree.read(cx).full_path(&self.path)
3233 let worktree = self.worktree.read(cx);
3234
3235 if worktree.is_visible() {
3236 full_path.push(worktree.root_name());
3237 } else {
3238 let path = worktree.abs_path();
3239
3240 if worktree.is_local() && path.starts_with(home_dir().as_path()) {
3241 full_path.push("~");
3242 full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
3243 } else {
3244 full_path.push(path)
3245 }
3246 }
3247
3248 if self.path.components().next().is_some() {
3249 full_path.push(&self.path);
3250 }
3251
3252 full_path
3253 }3258 }
3254 3259
3255 /// Returns the last component of this handle's absolute path. If this handle refers to the root3260 /// Returns the last component of this handle's absolute path. If this handle refers to the root