Shorten overflowing paths in file finder (#25303)
Closes #7711 This PR changes the file finder to shorten the path portion of each match by replacing a segment with `...`, if it would otherwise overflow horizontally. Details: - The overflow calculation is based on a crude linear width estimate for ASCII text at the current em width. No elision is done for non-ASCII paths. - A path component will not be elided if it contains a matching position for the file finder's search, or if it's the first or last component. - Elision is only applied when it is successful in shortening the path enough to not overflow. Release Notes: - Improved the appearance of the file finder when long paths are shown by eliding path segments
This commit is contained in:
parent
7ff40091d8
commit
aba89ba12a
2 changed files with 397 additions and 175 deletions
|
@ -16,6 +16,164 @@ fn init_logger() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_elision() {
|
||||
#[track_caller]
|
||||
fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
|
||||
let mut path = path.to_owned();
|
||||
let slice = PathComponentSlice::new(&path);
|
||||
let matches = Vec::from_iter(matches);
|
||||
if let Some(range) = slice.elision_range(budget - 1, &matches) {
|
||||
path.replace_range(range, "…");
|
||||
}
|
||||
assert_eq!(path, expected);
|
||||
}
|
||||
|
||||
// Simple cases, mostly to check that different path shapes are handled gracefully.
|
||||
check("p/a/b/c/d/", 6, [], "p/…/d/");
|
||||
check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
|
||||
check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
|
||||
check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
|
||||
|
||||
check("p/a/b/c/d", 5, [], "p/…/d");
|
||||
check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
|
||||
check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
|
||||
check("p/a/b/c/d", 7, [6], "p/…/c/d");
|
||||
|
||||
check("/p/a/b/c/d/", 7, [], "/p/…/d/");
|
||||
check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
|
||||
check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
|
||||
check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
|
||||
|
||||
// If the budget can't be met, no elision is done.
|
||||
check(
|
||||
"project/dir/child/grandchild",
|
||||
5,
|
||||
[],
|
||||
"project/dir/child/grandchild",
|
||||
);
|
||||
|
||||
// The longest unmatched segment is picked for elision.
|
||||
check(
|
||||
"project/one/two/X/three/sub",
|
||||
21,
|
||||
[16],
|
||||
"project/…/X/three/sub",
|
||||
);
|
||||
|
||||
// Elision stops when the budget is met, even though there are more components in the chosen segment.
|
||||
// It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
|
||||
check(
|
||||
"project/one/two/three/X/sub",
|
||||
21,
|
||||
[22],
|
||||
"project/…/three/X/sub",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_project_search_ordering_in_file_finder() {
|
||||
let mut file_finder_sorted_output = vec![
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 0.5,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 0.5,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
];
|
||||
file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
|
||||
|
||||
assert_eq!(
|
||||
file_finder_sorted_output,
|
||||
vec![
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("c1.0")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 0.5,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("a0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
ProjectPanelOrdMatch(PathMatch {
|
||||
score: 0.5,
|
||||
positions: Vec::new(),
|
||||
worktree_id: 0,
|
||||
path: Arc::from(Path::new("b0.5")),
|
||||
path_prefix: Arc::default(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue