Cargo.lock 🔗
@@ -3086,6 +3086,7 @@ dependencies = [
"rayon",
"release_channel",
"serde",
+ "serde_json",
"tempfile",
"util",
"windows 0.61.3",
Ulysse Buonomo and Syed Sadiq Ali created
Changes are made to `parse_path_with_position`:
we try to get the canonical, existing parts of
a path, then append the non-existing parts.
Closes #4441
Release Notes:
- Added the possibility to open a non-existing path using `zed` CLI
```
zed path/to/non/existing/file.txt
```
Co-authored-by: Syed Sadiq Ali <sadiqonemail@gmail.com>
Cargo.lock | 1
crates/cli/Cargo.toml | 4
crates/cli/src/main.rs | 187 ++++++++++++++++++++++++++++++++++++++-----
crates/zlog/README.md | 15 +++
script/debug-cli | 2
5 files changed, 185 insertions(+), 24 deletions(-)
@@ -3086,6 +3086,7 @@ dependencies = [
"rayon",
"release_channel",
"serde",
+ "serde_json",
"tempfile",
"util",
"windows 0.61.3",
@@ -34,6 +34,10 @@ util.workspace = true
tempfile.workspace = true
rayon.workspace = true
+[dev-dependencies]
+serde_json.workspace = true
+util = { workspace = true, features = ["test-support"] }
+
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
exec.workspace = true
fork.workspace = true
@@ -129,32 +129,173 @@ struct Args {
askpass: Option<String>,
}
+/// Parses a path containing a position (e.g. `path:line:column`)
+/// and returns its canonicalized string representation.
+///
+/// If a part of path doesn't exist, it will canonicalize the
+/// existing part and append the non-existing part.
+///
+/// This method must return an absolute path, as many zed
+/// crates assume absolute paths.
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
- let canonicalized = match Path::new(argument_str).canonicalize() {
- Ok(existing_path) => PathWithPosition::from_path(existing_path),
- Err(_) => {
- let path = PathWithPosition::parse_str(argument_str);
+ match Path::new(argument_str).canonicalize() {
+ Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
+ Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
let curdir = env::current_dir().context("retrieving current directory")?;
- path.map_path(|path| match fs::canonicalize(&path) {
- Ok(path) => Ok(path),
- Err(e) => {
- if let Some(mut parent) = path.parent() {
- if parent == Path::new("") {
- parent = &curdir
- }
- match fs::canonicalize(parent) {
- Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
- Err(_) => Err(e),
- }
- } else {
- Err(e)
- }
+ let mut children = Vec::new();
+ let root;
+ loop {
+ // canonicalize handles './', and '/'.
+ if let Ok(canonicalized) = fs::canonicalize(&path) {
+ root = canonicalized;
+ break;
}
- })
- }
- .with_context(|| format!("parsing as path with position {argument_str}"))?,
- };
- Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
+ // The comparison to `curdir` is just a shortcut
+ // since we know it is canonical. The other one
+ // is if `argument_str` is a string that starts
+ // with a name (e.g. "foo/bar").
+ if path == curdir || path == Path::new("") {
+ root = curdir;
+ break;
+ }
+ children.push(
+ path.file_name()
+ .with_context(|| format!("parsing as path with position {argument_str}"))?
+ .to_owned(),
+ );
+ if !path.pop() {
+ unreachable!("parsing as path with position {argument_str}");
+ }
+ }
+ Ok(children.iter().rev().fold(root, |mut path, child| {
+ path.push(child);
+ path
+ }))
+ }),
+ }
+ .map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+ use util::path;
+ use util::paths::SanitizedPath;
+ use util::test::TempTree;
+
+ macro_rules! assert_path_eq {
+ ($left:expr, $right:expr) => {
+ assert_eq!(
+ SanitizedPath::new(Path::new(&$left)),
+ SanitizedPath::new(Path::new(&$right))
+ )
+ };
+ }
+
+ fn cwd() -> PathBuf {
+ env::current_dir().unwrap()
+ }
+
+ static CWD_LOCK: Mutex<()> = Mutex::new(());
+
+ fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
+ let _lock = CWD_LOCK.lock();
+ let old_cwd = cwd();
+ env::set_current_dir(path)?;
+ let result = f();
+ env::set_current_dir(old_cwd)?;
+ result
+ }
+
+ #[test]
+ fn test_parse_non_existing_path() {
+ // Absolute path
+ let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
+ assert_path_eq!(result, path!("/non/existing/path.txt"));
+
+ // Absolute path in cwd
+ let path = cwd().join(path!("non/existing/path.txt"));
+ let expected = path.to_string_lossy().to_string();
+ let result = parse_path_with_position(&expected).unwrap();
+ assert_path_eq!(result, expected);
+
+ // Relative path
+ let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
+ assert_path_eq!(result, expected)
+ }
+
+ #[test]
+ fn test_parse_existing_path() {
+ let temp_tree = TempTree::new(json!({
+ "file.txt": "",
+ }));
+ let file_path = temp_tree.path().join("file.txt");
+ let expected = file_path.to_string_lossy().to_string();
+
+ // Absolute path
+ let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
+ assert_path_eq!(result, expected);
+
+ // Relative path
+ let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
+ assert_path_eq!(result, expected);
+ }
+
+ // NOTE:
+ // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
+ // we assume that they are not supported out of the box.
+ #[cfg(not(windows))]
+ #[test]
+ fn test_parse_symlink_file() {
+ let temp_tree = TempTree::new(json!({
+ "target.txt": "",
+ }));
+ let target_path = temp_tree.path().join("target.txt");
+ let symlink_path = temp_tree.path().join("symlink.txt");
+ std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
+
+ // Absolute path
+ let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
+ assert_eq!(result, target_path.to_string_lossy());
+
+ // Relative path
+ let result =
+ with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
+ assert_eq!(result, target_path.to_string_lossy());
+ }
+
+ #[cfg(not(windows))]
+ #[test]
+ fn test_parse_symlink_dir() {
+ let temp_tree = TempTree::new(json!({
+ "some": {
+ "dir": { // symlink target
+ "ec": {
+ "tory": {
+ "file.txt": "",
+ }}}}}));
+
+ let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
+ let expected = target_file_path.to_string_lossy();
+
+ let dir_path = temp_tree.path().join("some/dir");
+ let symlink_path = temp_tree.path().join("symlink");
+ std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
+
+ // Absolute path
+ let result =
+ parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
+ .unwrap();
+ assert_eq!(result, expected);
+
+ // Relative path
+ let result = with_cwd(temp_tree.path(), || {
+ parse_path_with_position("symlink/ec/tory/file.txt")
+ })
+ .unwrap();
+ assert_eq!(result, expected);
+ }
}
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
@@ -0,0 +1,15 @@
+# Zlog
+
+Use the `ZED_LOG` environment variable to control logging output for Zed
+applications and libraries. The variable accepts a comma-separated list of
+directives that specify logging levels for different modules (crates). The
+general format is for instance:
+
+```
+ZED_LOG=info,project=debug,agent=off
+```
+
+- Levels can be one of: `off`/`none`, `error`, `warn`, `info`, `debug`, or
+ `trace`.
+- You don't need to specify the global level, default is `trace` in the crate
+ and `info` set by `RUST_LOG` in Zed.
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
-cargo build; cargo run -p cli -- --foreground --zed=target/debug/zed "$@"
+cargo build -p zed && cargo run -p cli -- --foreground --zed=${CARGO_TARGET_DIR:-target}/debug/zed "$@"