cli: Allow opening non-existent paths (#43250)
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>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -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> {
|
||||
|
||||
15
crates/zlog/README.md
Normal file
15
crates/zlog/README.md
Normal file
@@ -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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user