diff --git a/Cargo.lock b/Cargo.lock index e41e973041bfe99dec6258e910e1108a842c0748..3eee0204971ef09650a7b54988542a1f5d4c59ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3086,6 +3086,7 @@ dependencies = [ "rayon", "release_channel", "serde", + "serde_json", "tempfile", "util", "windows 0.61.3", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 54f7ec4f5315a6529579353f3aa489925534d4ba..63e99a3ed25fad919e1a86a3a1917e3617ac2737 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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 diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 335e75ac4f5e43e63159fb26018849d8e0a22ced..7dd8a3253c9a0c8440d9342e5c0b3fd19e7f9828 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -129,32 +129,173 @@ struct Args { askpass: Option, } +/// 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 { - 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(path: &Path, f: impl FnOnce() -> anyhow::Result) -> anyhow::Result { + 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 { diff --git a/crates/zlog/README.md b/crates/zlog/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d0fef147cb0fb300e5a4cfd3936a97d0ee111fc --- /dev/null +++ b/crates/zlog/README.md @@ -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. diff --git a/script/debug-cli b/script/debug-cli index 1a40e703381441e87ab621837f4a61fa4741a6ce..65017cd4562adb00cf4f48b10edcdf2c92038b4c 100755 --- a/script/debug-cli +++ b/script/debug-cli @@ -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 "$@"