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
28402840 }
28412841 }
28422842 AssistantContext::Directory(directory_context) => {
2843− let path = directory_context.project_path.clone();
2843+ let project_path = directory_context.project_path(cx);
28442844 workspace.update(cx, |workspace, cx| {
28452845 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) {
28472847 cx.emit(project::Event::RevealInProjectPanel(entry.id));
28482848 }
28492849 })
crates/agent/src/context.rs+18−9
@@ -1,9 +1,9 @@
1−use std::{ops::Range, sync::Arc};
1+use std::{ops::Range, path::Path, sync::Arc};
22
33 use gpui::{App, Entity, SharedString};
44 use language::{Buffer, File};
55 use language_model::LanguageModelRequestMessage;
6−use project::ProjectPath;
6+use project::{ProjectPath, Worktree};
77 use serde::{Deserialize, Serialize};
88 use text::{Anchor, BufferId};
99 use ui::IconName;
@@ -69,10 +69,21 @@ pub struct FileContext { · 59 unmodified lines
6969 #[derive(Debug, Clone)]
7070 pub struct DirectoryContext {
7171 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.
7375 pub context_buffers: Vec<ContextBuffer>,
7476 }
7577
78+impl 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+
7687 #[derive(Debug, Clone)]
7788 pub struct SymbolContext {
7889 pub id: ContextId,
@@ -86,12 +97,11 @@ pub struct FetchedUrlContext { · 7 unmodified lines
8697 pub text: SharedString,
8798 }
8899
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−
92100 #[derive(Debug, Clone)]
93101 pub struct ThreadContext {
94102 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.
95105 pub thread: Entity<Thread>,
96106 pub text: SharedString,
97107 }
@@ -105,12 +115,11 @@ impl ThreadContext { · 7 unmodified lines
105115 }
106116 }
107117
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−
111118 #[derive(Clone)]
112119 pub struct ContextBuffer {
113120 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.
114123 pub buffer: Entity<Buffer>,
115124 pub file: Arc<dyn File>,
116125 pub version: clock::Global,
crates/agent/src/context_picker.rs+3−1
@@ -289,12 +289,14 @@ impl ContextPicker { · 288 unmodified lines
289289 path_prefix,
290290 } => {
291291 let context_store = self.context_store.clone();
292+ let worktree_id = project_path.worktree_id;
292293 let path = project_path.path.clone();
293294
294295 ContextMenuItem::custom_entry(
295296 move |_window, cx| {
296297 render_file_context_entry(
297298 ElementId::NamedInteger("ctx-recent".into(), ix),
299+ worktree_id,
298300 &path,
299301 &path_prefix,
300302 false,
@@ -466,7 +468,7 @@ fn recent_context_picker_entries( · 165 unmodified lines
466468 recent.extend(
467469 workspace
468470 .recent_navigation_history_iter(cx)
469− .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
471+ .filter(|(path, _)| !current_files.contains(path))
470472 .take(4)
471473 .filter_map(|(project_path, _)| {
472474 project
crates/agent/src/context_picker/file_context_picker.rs+16−7
@@ -189,6 +189,7 @@ impl PickerDelegate for FileContextPickerDelegate { · 188 unmodified lines
189189 .toggle_state(selected)
190190 .child(render_file_context_entry(
191191 ElementId::NamedInteger("file-ctx-picker".into(), ix),
192+ WorktreeId::from_usize(mat.worktree_id),
192193 &mat.path,
193194 &mat.path_prefix,
194195 mat.is_dir,
@@ -328,19 +329,26 @@ pub fn extract_file_name_and_directory( · 133 unmodified lines
328329
329330 pub fn render_file_context_entry(
330331 id: ElementId,
331− path: &Path,
332+ worktree_id: WorktreeId,
333+ path: &Arc<Path>,
332334 path_prefix: &Arc<str>,
333335 is_directory: bool,
334336 context_store: WeakEntity<ContextStore>,
335337 cx: &App,
336338 ) -> 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);
338340
339341 let added = context_store.upgrade().and_then(|context_store| {
342+ let project_path = ProjectPath {
343+ worktree_id,
344+ path: path.clone(),
345+ };
340346 if is_directory {
341− context_store.read(cx).includes_directory(path)
347+ context_store.read(cx).includes_directory(&project_path)
342348 } 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)
344352 }
345353 });
346354
@@ -380,8 +388,9 @@ pub fn render_file_context_entry( · 33 unmodified lines
380388 )
381389 .child(Label::new("Added").size(LabelSize::Small)),
382390 ),
383− FileInclusion::InDirectory(dir_name) => {
384− let dir_name = dir_name.to_string_lossy().into_owned();
391+ FileInclusion::InDirectory(directory_project_path) => {
392+ // TODO: Consider using worktree full_path to include worktree name.
393+ let directory_path = directory_project_path.path.to_string_lossy().into_owned();
385394
386395 el.child(
387396 h_flex()
@@ -395,7 +404,7 @@ pub fn render_file_context_entry( · 7 unmodified lines
395404 )
396405 .child(Label::new("Included").size(LabelSize::Small)),
397406 )
398− .tooltip(Tooltip::text(format!("in {dir_name}")))
407+ .tooltip(Tooltip::text(format!("in {directory_path}")))
399408 }
400409 })
401410 }
crates/agent/src/context_store.rs+55−48
@@ -1,5 +1,5 @@
11 use std::ops::Range;
2−use std::path::{Path, PathBuf};
2+use std::path::Path;
33 use std::sync::Arc;
44
55 use anyhow::{Context as _, Result, anyhow};
@@ -28,7 +28,7 @@ pub struct ContextStore { · 22 unmodified lines
2828 // TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
2929 next_context_id: ContextId,
3030 files: BTreeMap<BufferId, ContextId>,
31− directories: HashMap<PathBuf, ContextId>,
31+ directories: HashMap<ProjectPath, ContextId>,
3232 symbols: HashMap<ContextSymbolId, ContextId>,
3333 symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
3434 symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
@@ -93,7 +93,7 @@ impl ContextStore { · 58 unmodified lines
9393 let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
9494
9595 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) {
9797 Some(FileInclusion::Direct(context_id)) => {
9898 if remove_if_exists {
9999 this.remove_context(context_id, cx);
@@ -159,7 +159,7 @@ impl ContextStore { · 59 unmodified lines
159159 return Task::ready(Err(anyhow!("failed to read project")));
160160 };
161161
162− let already_included = match self.includes_directory(&project_path.path) {
162+ let already_included = match self.includes_directory(&project_path) {
163163 Some(FileInclusion::Direct(context_id)) => {
164164 if remove_if_exists {
165165 self.remove_context(context_id, cx);
@@ -223,14 +223,12 @@ impl ContextStore { · 57 unmodified lines
223223 .collect::<Vec<_>>();
224224
225225 if context_buffers.is_empty() {
226− return Err(anyhow!(
227− "No text files found in {}",
228− &project_path.path.display()
229− ));
226+ let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
227+ return Err(anyhow!("No text files found in {}", &full_path.display()));
230228 }
231229
232230 this.update(cx, |this, cx| {
233− this.insert_directory(project_path, context_buffers, cx);
231+ this.insert_directory(worktree, project_path, context_buffers, cx);
234232 })?;
235233
236234 anyhow::Ok(())
@@ -239,17 +237,20 @@ impl ContextStore { · 2 unmodified lines
239237
240238 fn insert_directory(
241239 &mut self,
240+ worktree: Entity<Worktree>,
242241 project_path: ProjectPath,
243242 context_buffers: Vec<ContextBuffer>,
244243 cx: &mut Context<Self>,
245244 ) {
246245 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);
248248
249249 self.context
250250 .push(AssistantContext::Directory(DirectoryContext {
251251 id,
252− project_path,
252+ worktree,
253+ path,
253254 context_buffers,
254255 }));
255256 cx.notify();
@@ -478,23 +479,31 @@ impl ContextStore { · 222 unmodified lines
478479 /// Returns whether the buffer is already included directly in the context, or if it will be
479480 /// included in the context via a directory. Directory inclusion is based on paths rather than
480481 /// 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> {
482487 if let Some(context_id) = self.files.get(&buffer_id) {
483488 return Some(FileInclusion::Direct(*context_id));
484489 }
485490
486− self.will_include_file_path_via_directory(path)
491+ self.will_include_file_path_via_directory(project_path)
487492 }
488493
489494 /// Returns whether this file path is already included directly in the context, or if it will be
490495 /// 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> {
492501 if !self.files.is_empty() {
493502 let found_file_context = self.context.iter().find(|context| match &context {
494503 AssistantContext::File(file_context) => {
495504 let buffer = file_context.context_buffer.buffer.read(cx);
496− if let Some(file_path) = buffer_path_log_err(buffer, cx) {
497− *file_path == *path
505+ if let Some(context_path) = buffer.project_path(cx) {
506+ &context_path == project_path
498507 } else {
499508 false
500509 }
@@ -506,31 +515,40 @@ impl ContextStore { · 5 unmodified lines
506515 }
507516 }
508517
509− self.will_include_file_path_via_directory(path)
518+ self.will_include_file_path_via_directory(project_path)
510519 }
511520
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> {
513525 if self.directories.is_empty() {
514526 return None;
515527 }
516528
517− let mut buf = path.to_path_buf();
529+ let mut path_buf = project_path.path.to_path_buf();
518530
519− while buf.pop() {
520− if let Some(_) = self.directories.get(&buf) {
521− return Some(FileInclusion::InDirectory(buf));
531+ while path_buf.pop() {
532+ // TODO: This isn't very efficient. Consider using a better representation of the
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));
522540 }
523541 }
524542
525543 None
526544 }
527545
528− pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
529− if let Some(context_id) = self.directories.get(path) {
546+ pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
547+ if let Some(context_id) = self.directories.get(project_path) {
530548 return Some(FileInclusion::Direct(*context_id));
531549 }
532550
533− self.will_include_file_path_via_directory(path)
551+ self.will_include_file_path_via_directory(project_path)
534552 }
535553
536554 pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
@@ -564,13 +582,13 @@ impl ContextStore { · 27 unmodified lines
564582 }
565583 }
566584
567− pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
585+ pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
568586 self.context
569587 .iter()
570588 .filter_map(|context| match context {
571589 AssistantContext::File(file) => {
572590 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)
574592 }
575593 AssistantContext::Directory(_)
576594 | AssistantContext::Symbol(_)
@@ -587,7 +605,7 @@ impl ContextStore { · 10 unmodified lines
587605
588606 pub enum FileInclusion {
589607 Direct(ContextId),
590− InDirectory(PathBuf),
608+ InDirectory(ProjectPath),
591609 }
592610
593611 // ContextBuffer without text.
@@ -654,19 +672,6 @@ fn collect_buffer_info_and_text( · 60 unmodified lines
654672 Ok((buffer_info, text_task))
655673 }
656674
657−pub 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−
670675 fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
671676 let path_extension = path.extension().and_then(|ext| ext.to_str());
672677 let path_string = path.to_string_lossy();
@@ -742,13 +747,13 @@ pub fn refresh_context_store_text( · 69 unmodified lines
742747 }
743748 }
744749 AssistantContext::Directory(directory_context) => {
750+ let directory_path = directory_context.project_path(cx);
745751 let should_refresh = changed_buffers.is_empty()
746752 || changed_buffers.iter().any(|buffer| {
747− let buffer = buffer.read(cx);
748−
749− buffer_path_log_err(&buffer, cx).map_or(false, |path| {
750− path.starts_with(&directory_context.project_path.path)
751− })
753+ let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
754+ return false;
755+ };
756+ buffer_path.starts_with(&directory_path)
752757 });
753758
754759 if should_refresh {
@@ -835,14 +840,16 @@ fn refresh_directory_text( · 80 unmodified lines
835840 let context_buffers = future::join_all(futures);
836841
837842 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();
839845 Some(cx.spawn(async move |cx| {
840846 let context_buffers = context_buffers.await;
841847 context_store
842848 .update(cx, |context_store, _| {
843849 let new_directory_context = DirectoryContext {
844850 id,
845− project_path,
851+ worktree,
852+ path,
846853 context_buffers,
847854 };
848855 context_store.replace_context(AssistantContext::Directory(new_directory_context));
crates/agent/src/context_strip.rs+7−8
@@ -1,3 +1,4 @@
1+use std::path::Path;
12 use std::rc::Rc;
23
34 use collections::HashSet;
@@ -9,6 +10,7 @@ use gpui::{ · 5 unmodified lines
910 };
1011 use itertools::Itertools;
1112 use language::Buffer;
13+use project::ProjectItem;
1214 use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
1315 use workspace::{Workspace, notifications::NotifyResultExt};
1416
@@ -93,26 +95,23 @@ impl ContextStrip { · 78 unmodified lines
9395 let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
9496 let active_buffer = active_buffer_entity.read(cx);
9597
96− let path = active_buffer.file()?.full_path(cx);
98+ let project_path = active_buffer.project_path(cx)?;
9799
98100 if self
99101 .context_store
100102 .read(cx)
101− .will_include_buffer(active_buffer.remote_id(), &path)
103+ .will_include_buffer(active_buffer.remote_id(), &project_path)
102104 .is_some()
103105 {
104106 return None;
105107 }
106108
107− let name = match path.file_name() {
108− Some(name) => name.to_string_lossy().into_owned().into(),
109− None => path.to_string_lossy().into_owned().into(),
110− };
109+ let file_name = active_buffer.file()?.file_name(cx);
111110
112− let icon_path = FileIcons::get_icon(&path, cx);
111+ let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
113112
114113 Some(SuggestedContext::File {
115− name,
114+ name: file_name.to_string_lossy().into_owned().into(),
116115 buffer: active_buffer_entity.downgrade(),
117116 icon_path,
118117 })
crates/agent/src/ui/context_pill.rs+4−3
@@ -280,9 +280,10 @@ impl AddedContext { · 279 unmodified lines
280280 }
281281
282282 AssistantContext::Directory(directory_context) => {
283− // TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
284− // handle renames?
285− let full_path = &directory_context.project_path.path;
283+ let full_path = directory_context
284+ .worktree
285+ .read(cx)
286+ .full_path(&directory_context.path);
286287 let full_path_string: SharedString =
287288 full_path.to_string_lossy().into_owned().into();
288289 let name = full_path
crates/project/src/project.rs+4−0
@@ -334,6 +334,10 @@ impl ProjectPath { · 333 unmodified lines
334334 path: Path::new("").into(),
335335 }
336336 }
337+
338+ pub fn starts_with(&self, other: &ProjectPath) -> bool {
339+ self.worktree_id == other.worktree_id && self.path.starts_with(&other.path)
340+ }
337341 }
338342
339343 #[derive(Debug, Default)]
crates/worktree/src/worktree.rs+26−21
@@ -1172,6 +1172,31 @@ impl Worktree { · 1171 unmodified lines
11721172 pub fn is_single_file(&self) -> bool {
11731173 self.root_dir().is_none()
11741174 }
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+ }
11751200 }
11761201
11771202 impl LocalWorktree {
@@ -3229,27 +3254,7 @@ impl language::File for File { · 2051 unmodified lines
32293254 }
32303255
32313256 fn full_path(&self, cx: &App) -> PathBuf {
3232− let mut full_path = PathBuf::new();
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
3257+ self.worktree.read(cx).full_path(&self.path)
32533258 }
32543259
32553260 /// Returns the last component of this handle's absolute path. If this handle refers to the root