on master help sign in
Canonicalize paths before stripping current dir as prefix (#6290)
d04288ejazzfool
Cargo.lock+1
@@ -1082,6 +1082,7 @@ dependencies = [
1082 "arc-swap",
1082 "arc-swap",
1083 "bitflags 2.0.2",
1083 "bitflags 2.0.2",
1084 "chrono",
1084 "chrono",
1085+ "dunce",
1085 "encoding_rs",
1086 "encoding_rs",
1086 "etcetera",
1087 "etcetera",
1087 "hashbrown 0.13.2",
1088 "hashbrown 0.13.2",
helix-core/Cargo.toml+1
@@ -32,6 +32,7 @@ regex = "1"
32bitflags = "2.0"
32bitflags = "2.0"
33ahash = "0.8.3"
33ahash = "0.8.3"
34hashbrown = { version = "0.13.2", features = ["raw"] }
34hashbrown = { version = "0.13.2", features = ["raw"] }
35+dunce = "1.0"
35
36
36log = "0.4"
37log = "0.4"
37serde = { version = "1.0", features = ["derive"] }
38serde = { version = "1.0", features = ["derive"] }
helix-core/src/path.rs+25−4
@@ -40,6 +40,21 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
40/// needs to improve on.
40/// needs to improve on.
41/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
41/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
42pub fn get_normalized_path(path: &Path) -> PathBuf {
42pub fn get_normalized_path(path: &Path) -> PathBuf {
43+ // normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible),
44+ // then run handrolled normalization on the non-existent remainder
45+ let (base, path) = path
46+ .ancestors()
47+ .find_map(|base| {
48+ let canonicalized_base = dunce::canonicalize(base).ok()?;
49+ let remainder = path.strip_prefix(base).ok()?.into();
50+ Some((canonicalized_base, remainder))
51+ })
52+ .unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path)));
53+
54+ if path.as_os_str().is_empty() {
55+ return base;
56+ }
57+
43 let mut components = path.components().peekable();
58 let mut components = path.components().peekable();
44 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
59 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
45 components.next();
60 components.next();
@@ -63,7 +78,7 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
63 }
78 }
64 }
79 }
65 }
80 }
66− ret
81+ base.join(ret)
67}
82}
68
83
69/// Returns the canonical, absolute form of a path with all intermediate components normalized.
84/// Returns the canonical, absolute form of a path with all intermediate components normalized.
@@ -82,13 +97,19 @@ pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
82}
97}
83
98
84pub fn get_relative_path(path: &Path) -> PathBuf {
99pub fn get_relative_path(path: &Path) -> PathBuf {
100+ let path = PathBuf::from(path);
85 let path = if path.is_absolute() {
101 let path = if path.is_absolute() {
102+ let cwdir = std::env::current_dir()
103+ .map(|path| get_normalized_path(&path))
86− let cwdir = std::env::current_dir().expect("couldn't determine current directory");
104+ .expect("couldn't determine current directory");
105+ get_normalized_path(&path)
87− path.strip_prefix(cwdir).unwrap_or(path)
106+ .strip_prefix(cwdir)
107+ .map(PathBuf::from)
108+ .unwrap_or(path)
88 } else {
109 } else {
89 path
110 path
90 };
111 };
91− fold_home_dir(path)
112+ fold_home_dir(&path)
92}
113}
93
114
94/// Returns a truncated filepath where the basepart of the path is reduced to the first
115/// Returns a truncated filepath where the basepart of the path is reduced to the first
helix-term/tests/integration.rs−2
@@ -2,8 +2,6 @@
2mod test {
2mod test {
3 mod helpers;
3 mod helpers;
4
4
5− use std::path::PathBuf;
6−
7 use helix_core::{syntax::AutoPairConfig, Selection};
5 use helix_core::{syntax::AutoPairConfig, Selection};
8 use helix_term::config::Config;
6 use helix_term::config::Config;
9
7
helix-term/tests/test/commands/write.rs+5−5
@@ -3,7 +3,7 @@ use std::{
3 ops::RangeInclusive,
3 ops::RangeInclusive,
4};
4};
5
5
6−use helix_core::diagnostic::Severity;
6+use helix_core::{diagnostic::Severity, path::get_normalized_path};
7use helix_view::doc;
7use helix_view::doc;
8
8
9use super::*;
9use super::*;
@@ -23,7 +23,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> {
23 assert_eq!(1, docs.len());
23 assert_eq!(1, docs.len());
24
24
25 let doc = docs.pop().unwrap();
25 let doc = docs.pop().unwrap();
26− assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path));
26+ assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
27 assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
27 assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
28 }),
28 }),
29 false,
29 false,
@@ -269,7 +269,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
269 assert_eq!(1, docs.len());
269 assert_eq!(1, docs.len());
270
270
271 let doc = docs.pop().unwrap();
271 let doc = docs.pop().unwrap();
272− assert_eq!(Some(&file.path().to_path_buf()), doc.path());
272+ assert_eq!(Some(&get_normalized_path(file.path())), doc.path());
273 }),
273 }),
274 false,
274 false,
275 )
275 )
@@ -341,7 +341,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
341 Some(&|app| {
341 Some(&|app| {
342 let doc = doc!(app.editor);
342 let doc = doc!(app.editor);
343 assert!(!app.editor.is_err());
343 assert!(!app.editor.is_err());
344− assert_eq!(file1.path(), doc.path().unwrap());
344+ assert_eq!(&get_normalized_path(file1.path()), doc.path().unwrap());
345 }),
345 }),
346 ),
346 ),
347 (
347 (
@@ -349,7 +349,7 @@ async fn test_write_new_path() -> anyhow::Result<()> {
349 Some(&|app| {
349 Some(&|app| {
350 let doc = doc!(app.editor);
350 let doc = doc!(app.editor);
351 assert!(!app.editor.is_err());
351 assert!(!app.editor.is_err());
352− assert_eq!(file2.path(), doc.path().unwrap());
352+ assert_eq!(&get_normalized_path(file2.path()), doc.path().unwrap());
353 assert!(app.editor.document_by_path(file1.path()).is_none());
353 assert!(app.editor.document_by_path(file1.path()).is_none());
354 }),
354 }),
355 ),
355 ),
helix-term/tests/test/splits.rs+5−3
@@ -1,5 +1,7 @@
1use super::*;
1use super::*;
2
2
3+use helix_core::path::get_normalized_path;
4+
3#[tokio::test(flavor = "multi_thread")]
5#[tokio::test(flavor = "multi_thread")]
4async fn test_split_write_quit_all() -> anyhow::Result<()> {
6async fn test_split_write_quit_all() -> anyhow::Result<()> {
5 let mut file1 = tempfile::NamedTempFile::new()?;
7 let mut file1 = tempfile::NamedTempFile::new()?;
@@ -25,21 +27,21 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
25
27
26 let doc1 = docs
28 let doc1 = docs
27 .iter()
29 .iter()
28− .find(|doc| doc.path().unwrap() == file1.path())
30+ .find(|doc| doc.path().unwrap() == &get_normalized_path(file1.path()))
29 .unwrap();
31 .unwrap();
30
32
31 assert_eq!("hello1", doc1.text().to_string());
33 assert_eq!("hello1", doc1.text().to_string());
32
34
33 let doc2 = docs
35 let doc2 = docs
34 .iter()
36 .iter()
35− .find(|doc| doc.path().unwrap() == file2.path())
37+ .find(|doc| doc.path().unwrap() == &get_normalized_path(file2.path()))
36 .unwrap();
38 .unwrap();
37
39
38 assert_eq!("hello2", doc2.text().to_string());
40 assert_eq!("hello2", doc2.text().to_string());
39
41
40 let doc3 = docs
42 let doc3 = docs
41 .iter()
43 .iter()
42− .find(|doc| doc.path().unwrap() == file3.path())
44+ .find(|doc| doc.path().unwrap() == &get_normalized_path(file3.path()))
43 .unwrap();
45 .unwrap();
44
46
45 assert_eq!("hello3", doc3.text().to_string());
47 assert_eq!("hello3", doc3.text().to_string());