Fix opening file with colon (#17281)

Closes #14100

Release Notes:

- Fixed unable to open file with a colon from Zed CLI

-----

I didn't make change to tests for the first two commits. I changed them
to easily find offending test cases. Behavior changes are in last commit
message.

In the last commit, I changed how `PathWithPosition` should intreprete
file paths. If my assumptions are off, please advise so that I can make
another approach.

I also believe further constraints would be better for
`PathWithPosition`'s intention. But people can make future improvements
to `PathWithPosition`.
This commit is contained in:
Erick Guan 2024-09-17 17:19:07 +02:00 committed by GitHub
parent ddaee2e8dd
commit ecd1830793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -98,10 +98,6 @@ impl<T: AsRef<Path>> PathExt for T {
/// A delimiter to use in `path_query:row_number:column_number` strings parsing. /// A delimiter to use in `path_query:row_number:column_number` strings parsing.
pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
/// Extracts filename and row-column suffixes.
/// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
// NOTE: All cases need to have exactly three capture groups for extract(): file_name, row and column.
// Valid patterns that don't contain row and/or column should have empty groups in their place.
const ROW_COL_CAPTURE_REGEX: &str = r"(?x) const ROW_COL_CAPTURE_REGEX: &str = r"(?x)
([^\(]+)(?: ([^\(]+)(?:
\((\d+),(\d+)\) # filename(row,column) \((\d+),(\d+)\) # filename(row,column)
@ -109,12 +105,12 @@ const ROW_COL_CAPTURE_REGEX: &str = r"(?x)
\((\d+)\)() # filename(row) \((\d+)\)() # filename(row)
) )
| |
([^\:]+)(?: (.+?)(?:
\:(\d+)\:(\d+) # filename:row:column \:+(\d+)\:(\d+)\:*$ # filename:row:column
| |
\:(\d+)() # filename:row \:+(\d+)\:*()$ # filename:row
| |
\:()() # filename: \:*()()$ # filename:
)"; )";
/// A representation of a path-like string with optional row and column numbers. /// A representation of a path-like string with optional row and column numbers.
@ -136,9 +132,92 @@ impl PathWithPosition {
column: None, column: None,
} }
} }
/// Parses a string that possibly has `:row:column` or `(row, column)` suffix. /// Parses a string that possibly has `:row:column` or `(row, column)` suffix.
/// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools
/// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
/// If the suffix parsing fails, the whole string is parsed as a path. /// If the suffix parsing fails, the whole string is parsed as a path.
///
/// Be mindful that `test_file:10:1:` is a valid posix filename.
/// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename.
///
/// # Examples
///
/// ```
/// # use util::paths::PathWithPosition;
/// # use std::path::PathBuf;
/// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition {
/// path: PathBuf::from("test_file"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition {
/// path: PathBuf::from("test_file"),
/// row: Some(10),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: Some(2),
/// });
/// ```
///
/// # Expected parsing results when encounter ill-formatted inputs.
/// ```
/// # use util::paths::PathWithPosition;
/// # use std::path::PathBuf;
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:a"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:a:b"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: None,
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs"),
/// row: Some(1),
/// column: Some(2),
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:1"),
/// row: Some(2),
/// column: None,
/// });
/// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition {
/// path: PathBuf::from("test_file.rs:1"),
/// row: Some(2),
/// column: Some(3),
/// });
/// ```
pub fn parse_str(s: &str) -> Self { pub fn parse_str(s: &str) -> Self {
let trimmed = s.trim(); let trimmed = s.trim();
let path = Path::new(trimmed); let path = Path::new(trimmed);
@ -359,206 +438,229 @@ mod tests {
} }
#[test] #[test]
fn path_with_position_parsing_positive() { fn path_with_position_parse_posix_path() {
let input_and_expected = [ // Test POSIX filename edge cases
( // Read more at https://en.wikipedia.org/wiki/Filename
"test_file.rs", assert_eq!(
PathWithPosition { PathWithPosition::parse_str(" test_file"),
path: PathBuf::from("test_file.rs"), PathWithPosition {
row: None, path: PathBuf::from("test_file"),
column: None, row: None,
}, column: None
), }
( );
"test_file.rs:1",
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: None,
},
),
(
"test_file.rs:1:2",
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: Some(2),
},
),
];
for (input, expected) in input_and_expected { assert_eq!(
let actual = PathWithPosition::parse_str(input); PathWithPosition::parse_str("a:bc:.zip:1"),
assert_eq!( PathWithPosition {
actual, expected, path: PathBuf::from("a:bc:.zip"),
"For positive case input str '{input}', got a parse mismatch" row: Some(1),
); column: None
} }
);
assert_eq!(
PathWithPosition::parse_str("one.second.zip:1"),
PathWithPosition {
path: PathBuf::from("one.second.zip"),
row: Some(1),
column: None
}
);
// Trim off trailing `:`s for otherwise valid input.
assert_eq!(
PathWithPosition::parse_str("test_file:10:1:"),
PathWithPosition {
path: PathBuf::from("test_file"),
row: Some(10),
column: Some(1)
}
);
assert_eq!(
PathWithPosition::parse_str("test_file.rs:"),
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: None,
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("test_file.rs:1:"),
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: None
}
);
} }
#[test] #[test]
fn path_with_position_parsing_negative() { #[cfg(not(target_os = "windows"))]
for (input, row, column) in [ fn path_with_position_parse_posix_path_with_suffix() {
("test_file.rs:a", None, None), assert_eq!(
("test_file.rs:a:b", None, None), PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"),
("test_file.rs::", None, None), PathWithPosition {
("test_file.rs::1", None, None), path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"),
("test_file.rs:1::", Some(1), None), row: Some(34),
("test_file.rs::1:2", None, None), column: None,
("test_file.rs:1::2", Some(1), None), }
("test_file.rs:1:2:3", Some(1), Some(2)), );
] {
let actual = PathWithPosition::parse_str(input); assert_eq!(
assert_eq!( PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"),
actual, PathWithPosition {
PathWithPosition { path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
path: PathBuf::from("test_file.rs"), row: Some(1902),
row, column: Some(13),
column, }
}, );
"For negative case input str '{input}', got a parse mismatch"
); assert_eq!(
} PathWithPosition::parse_str("crate/utils/src/test:today.log:34"),
PathWithPosition {
path: PathBuf::from("crate/utils/src/test:today.log"),
row: Some(34),
column: None,
}
);
} }
// Trim off trailing `:`s for otherwise valid input.
#[test] #[test]
fn path_with_position_parsing_special() { #[cfg(target_os = "windows")]
#[cfg(not(target_os = "windows"))] fn path_with_position_parse_windows_path() {
let input_and_expected = [ assert_eq!(
( PathWithPosition::parse_str("crates\\utils\\paths.rs"),
"test_file.rs:", PathWithPosition {
PathWithPosition { path: PathBuf::from("crates\\utils\\paths.rs"),
path: PathBuf::from("test_file.rs"), row: None,
row: None, column: None
column: None, }
}, );
),
(
"test_file.rs:1:",
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: None,
},
),
(
"crates/file_finder/src/file_finder.rs:1902:13:",
PathWithPosition {
path: PathBuf::from("crates/file_finder/src/file_finder.rs"),
row: Some(1902),
column: Some(13),
},
),
];
#[cfg(target_os = "windows")] assert_eq!(
let input_and_expected = [ PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"),
( PathWithPosition {
"test_file.rs:", path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
PathWithPosition { row: None,
path: PathBuf::from("test_file.rs"), column: None
row: None, }
column: None, );
}, }
),
(
"test_file.rs:1:",
PathWithPosition {
path: PathBuf::from("test_file.rs"),
row: Some(1),
column: None,
},
),
(
"\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:",
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
},
),
(
"\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:",
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
},
),
(
"\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:",
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
},
),
(
"\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):",
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
},
),
(
"\\\\?\\C:\\Users\\someone\\test_file.rs(1902):",
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
},
),
(
"C:\\Users\\someone\\test_file.rs:1902:13:",
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
},
),
(
"crates/utils/paths.rs",
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: None,
column: None,
},
),
(
"C:\\Users\\someone\\test_file.rs(1902,13):",
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
},
),
(
"C:\\Users\\someone\\test_file.rs(1902):",
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
},
),
(
"crates/utils/paths.rs:101",
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: Some(101),
column: None,
},
),
];
for (input, expected) in input_and_expected { #[test]
let actual = PathWithPosition::parse_str(input); #[cfg(target_os = "windows")]
assert_eq!( fn path_with_position_parse_windows_path_with_suffix() {
actual, expected, assert_eq!(
"For special case input str '{input}', got a parse mismatch" PathWithPosition::parse_str("crates\\utils\\paths.rs:101"),
); PathWithPosition {
} path: PathBuf::from("crates\\utils\\paths.rs"),
row: Some(101),
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1),
column: Some(20)
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13)
}
);
// Trim off trailing `:`s for otherwise valid input.
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13)
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
row: Some(13),
column: Some(15)
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"),
row: Some(15),
column: None
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"),
PathWithPosition {
path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: Some(13),
}
);
assert_eq!(
PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"),
PathWithPosition {
path: PathBuf::from("C:\\Users\\someone\\test_file.rs"),
row: Some(1902),
column: None,
}
);
assert_eq!(
PathWithPosition::parse_str("crates/utils/paths.rs:101"),
PathWithPosition {
path: PathBuf::from("crates\\utils\\paths.rs"),
row: Some(101),
column: None,
}
);
} }
#[test] #[test]