已合并
fix(diff): 文件修改列表统一显示 workspace 相对路径 #465
fix(diff): 文件修改列表统一显示 workspace 相对路径 #465
已合并
共 1 个文件变更+26-3
| @@ -1,6 +1,6 @@ | |||
| 1 | use std::collections::{BTreeMap, HashMap}; | 1 | use std::collections::{BTreeMap, HashMap}; |
| 2 | use std::fs; | 2 | use std::fs; |
| 3 | -use std::path::{Path, PathBuf}; | 3 | +use std::path::{Component, Path, PathBuf}; |
| 4 | 4 | ||
| 5 | use serde::{Deserialize, Serialize}; | 5 | use serde::{Deserialize, Serialize}; |
| 6 | 6 | ||
| @@ -338,8 +338,12 @@ impl SessionDiffTracker { | |||
| 338 | } | 338 | } |
| 339 | 339 | ||
| 340 | pub fn display_file_path(&self, file_path: &str) -> String { | 340 | pub fn display_file_path(&self, file_path: &str) -> String { |
| 341 | - let path = Path::new(file_path); | 341 | + // Tool calls report both absolute and workspace-relative paths. Resolve |
| 342 | - if let Ok(relative) = path.strip_prefix(&self.workspace) { | 342 | + // either form under the workspace, then compare the two lexically: no |
| 343 | + // filesystem access here, this runs for every visible entry each frame. | ||
| 344 | + let workspace = lexical_path(&self.workspace); | ||
| 345 | + let absolute = lexical_path(&resolve_workspace_file_path(&workspace, file_path)); | ||
| 346 | + if let Ok(relative) = absolute.strip_prefix(&workspace) { | ||
| 343 | let display = relative.display().to_string(); | 347 | let display = relative.display().to_string(); |
| 344 | if !display.is_empty() { | 348 | if !display.is_empty() { |
| 345 | return display; | 349 | return display; |
| @@ -461,6 +465,25 @@ pub fn file_change_delta_from_tool_args(tool: &str, args_preview: &str) -> Optio | |||
| 461 | }) | 465 | }) |
| 462 | } | 466 | } |
| 463 | 467 | ||
| 468 | +/// Lexically normalize a path: drop `.` components and collapse `..` where it can | ||
| 469 | +/// be resolved without touching the filesystem (symlinks are NOT resolved). | ||
| 470 | +fn lexical_path(path: &Path) -> PathBuf { | ||
| 471 | + let mut out = PathBuf::new(); | ||
| 472 | + for component in path.components() { | ||
| 473 | + match component { | ||
| 474 | + Component::CurDir => {} | ||
| 475 | + Component::ParentDir => { | ||
| 476 | + let popped = out.pop(); | ||
| 477 | + if !popped { | ||
| 478 | + out.push(component.as_os_str()); | ||
| 479 | + } | ||
| 480 | + } | ||
| 481 | + other => out.push(other.as_os_str()), | ||
| 482 | + } | ||
| 483 | + } | ||
| 484 | + out | ||
| 485 | +} | ||
| 486 | + | ||
| 464 | fn resolve_workspace_file_path(workspace: &Path, file_path: &str) -> PathBuf { | 487 | fn resolve_workspace_file_path(workspace: &Path, file_path: &str) -> PathBuf { |
| 465 | let path = Path::new(file_path); | 488 | let path = Path::new(file_path); |
| 466 | if path.is_absolute() { | 489 | if path.is_absolute() { |