Release Notes: - N/A
Unified · Split
| @@ -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 | }) |
| @@ -1,9 +1,9 @@ | |||
| 1 | use std::{ops::Range, sync::Arc}; | 1 | use std::{ops::Range, path::Path, sync::Arc}; |
| 2 | 2 | ||
| 3 | use gpui::{App, Entity, SharedString}; | 3 | use gpui::{App, Entity, SharedString}; |
| 4 | use language::{Buffer, File}; | 4 | use language::{Buffer, File}; |
| 5 | use language_model::LanguageModelRequestMessage; | 5 | use language_model::LanguageModelRequestMessage; |
| 6 | use project::ProjectPath; | 6 | use project::{ProjectPath, Worktree}; |
| 7 | use serde::{Deserialize, Serialize}; | 7 | use serde::{Deserialize, Serialize}; |
| 8 | use text::{Anchor, BufferId}; | 8 | use text::{Anchor, BufferId}; |
| 9 | use ui::IconName; | 9 | use ui::IconName; |
| @@ -69,10 +69,21 @@ pub struct FileContext { · 59 unmodified lines | |||
| 69 | #[derive(Debug, Clone)] | 69 | #[derive(Debug, Clone)] |
| 70 | pub struct DirectoryContext { | 70 | pub 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 | ||
| 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 | |||
| 76 | #[derive(Debug, Clone)] | 87 | #[derive(Debug, Clone)] |
| 77 | pub struct SymbolContext { | 88 | pub 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)] |
| 93 | pub struct ThreadContext { | 101 | pub 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)] |
| 112 | pub struct ContextBuffer { | 119 | pub 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, |
| @@ -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 | workspace | 469 | 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 | project | 474 | project |
| @@ -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 | ||
| 329 | pub fn render_file_context_entry( | 330 | pub 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 | } |
| @@ -1,5 +1,5 @@ | |||
| 1 | use std::ops::Range; | 1 | use std::ops::Range; |
| 2 | use std::path::{Path, PathBuf}; | 2 | use std::path::Path; |
| 3 | use std::sync::Arc; | 3 | use std::sync::Arc; |
| 4 | 4 | ||
| 5 | use anyhow::{Context as _, Result, anyhow}; | 5 | use 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.context | 249 | 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 be | 479 | /// 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 than | 480 | /// 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 be | 494 | /// 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 == *path | 506 | &context_path == project_path |
| 498 | } else { | 507 | } else { |
| 499 | false | 508 | 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 | None | 543 | 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.context | 586 | 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 | ||
| 588 | pub enum FileInclusion { | 606 | pub 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 | ||
| 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 | |||
| 670 | fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString { | 675 | fn 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_store | 847 | 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)); |
| @@ -1,3 +1,4 @@ | |||
| 1 | use std::path::Path; | ||
| 1 | use std::rc::Rc; | 2 | use std::rc::Rc; |
| 2 | 3 | ||
| 3 | use collections::HashSet; | 4 | use collections::HashSet; |
| @@ -9,6 +10,7 @@ use gpui::{ · 5 unmodified lines | |||
| 9 | }; | 10 | }; |
| 10 | use itertools::Itertools; | 11 | use itertools::Itertools; |
| 11 | use language::Buffer; | 12 | use language::Buffer; |
| 13 | use project::ProjectItem; | ||
| 12 | use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; | 14 | use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; |
| 13 | use workspace::{Workspace, notifications::NotifyResultExt}; | 15 | use 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 self | 100 | if self |
| 99 | .context_store | 101 | .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 | }) |
| @@ -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 also | 283 | 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_path | 289 | let name = full_path |
| @@ -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)] |
| @@ -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 | ||
| 1177 | impl LocalWorktree { | 1202 | impl 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 root | 3260 | /// Returns the last component of this handle's absolute path. If this handle refers to the root |