Unify path:row:column parsing, use it in CLI

This commit is contained in:
Kirill Bulatov 2023-05-12 16:52:07 +03:00
parent 89fe5c6b09
commit d719352152
9 changed files with 127 additions and 65 deletions

2
Cargo.lock generated
View File

@ -1097,6 +1097,7 @@ dependencies = [
"plist", "plist",
"serde", "serde",
"serde_derive", "serde_derive",
"util",
] ]
[[package]] [[package]]
@ -2675,6 +2676,7 @@ dependencies = [
"postage", "postage",
"settings", "settings",
"text", "text",
"util",
"workspace", "workspace",
] ]

View File

@ -19,6 +19,7 @@ dirs = "3.0"
ipc-channel = "0.16" ipc-channel = "0.16"
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
util = { path = "../util" }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9" core-foundation = "0.9"

View File

@ -16,6 +16,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
ptr, ptr,
}; };
use util::paths::PathLikeWithPosition;
#[derive(Parser)] #[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
@ -24,8 +25,11 @@ struct Args {
#[clap(short, long)] #[clap(short, long)]
wait: bool, wait: bool,
/// A sequence of space-separated paths that you want to open. /// A sequence of space-separated paths that you want to open.
#[clap()] ///
paths: Vec<PathBuf>, /// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
#[clap(value_parser = parse_path_with_position)]
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
/// Print Zed's version and the app path. /// Print Zed's version and the app path.
#[clap(short, long)] #[clap(short, long)]
version: bool, version: bool,
@ -34,6 +38,14 @@ struct Args {
bundle_path: Option<PathBuf>, bundle_path: Option<PathBuf>,
} }
fn parse_path_with_position(
argument_str: &str,
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
PathLikeWithPosition::parse_str(argument_str, |path_str| {
Ok(Path::new(path_str).to_path_buf())
})
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct InfoPlist { struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")] #[serde(rename = "CFBundleShortVersionString")]
@ -50,7 +62,11 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
for path in args.paths.iter() { for path in args
.paths_with_position
.iter()
.map(|path_with_position| &path_with_position.path_like)
{
if !path.exists() { if !path.exists() {
touch(path.as_path())?; touch(path.as_path())?;
} }
@ -60,9 +76,13 @@ fn main() -> Result<()> {
tx.send(CliRequest::Open { tx.send(CliRequest::Open {
paths: args paths: args
.paths .paths_with_position
.into_iter() .into_iter()
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error))) // TODO kb continue sendint path with the position further
.map(|path_with_position| path_with_position.path_like)
.map(|path| {
fs::canonicalize(&path).with_context(|| format!("path {path:?} canonicalization"))
})
.collect::<Result<Vec<PathBuf>>>()?, .collect::<Result<Vec<PathBuf>>>()?,
wait: args.wait, wait: args.wait,
})?; })?;

View File

@ -98,7 +98,6 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
#[derive(Clone, Deserialize, PartialEq, Default)] #[derive(Clone, Deserialize, PartialEq, Default)]
pub struct SelectNext { pub struct SelectNext {

View File

@ -2,7 +2,6 @@ use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FILE_ROW_COLUMN_DELIMITER,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use collections::HashSet; use collections::HashSet;
@ -28,7 +27,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use text::Selection; use text::Selection;
use util::{ResultExt, TryFutureExt}; use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},

View File

@ -1,4 +1,4 @@
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor, FILE_ROW_COLUMN_DELIMITER}; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{ use gpui::{
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
@ -14,7 +14,7 @@ use std::{
}, },
}; };
use text::Point; use text::Point;
use util::{post_inc, ResultExt}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace; use workspace::Workspace;
pub type FileFinder = Picker<FileFinderDelegate>; pub type FileFinder = Picker<FileFinderDelegate>;
@ -25,7 +25,7 @@ pub struct FileFinderDelegate {
search_count: usize, search_count: usize,
latest_search_id: usize, latest_search_id: usize,
latest_search_did_cancel: bool, latest_search_did_cancel: bool,
latest_search_query: Option<FileSearchQuery>, latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
relative_to: Option<Arc<Path>>, relative_to: Option<Arc<Path>>,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>, selected: Option<(usize, Arc<Path>)>,
@ -66,31 +66,9 @@ pub enum Event {
struct FileSearchQuery { struct FileSearchQuery {
raw_query: String, raw_query: String,
file_query_end: Option<usize>, file_query_end: Option<usize>,
file_row: Option<u32>,
file_column: Option<u32>,
} }
impl FileSearchQuery { impl FileSearchQuery {
fn new(raw_query: String) -> Self {
let mut components = raw_query
.as_str()
.splitn(3, FILE_ROW_COLUMN_DELIMITER)
.map(str::trim)
.fuse();
let file_query = components.next().filter(|str| !str.is_empty());
let file_row = components.next().and_then(|row| row.parse::<u32>().ok());
let file_column = components.next().and_then(|col| col.parse::<u32>().ok());
Self {
file_query_end: file_query
.filter(|_| file_row.is_some())
.map(|query| query.len()),
file_row,
file_column,
raw_query,
}
}
fn path_query(&self) -> &str { fn path_query(&self) -> &str {
match self.file_query_end { match self.file_query_end {
Some(file_path_end) => &self.raw_query[..file_path_end], Some(file_path_end) => &self.raw_query[..file_path_end],
@ -152,7 +130,7 @@ impl FileFinderDelegate {
fn spawn_search( fn spawn_search(
&mut self, &mut self,
query: FileSearchQuery, query: PathLikeWithPosition<FileSearchQuery>,
cx: &mut ViewContext<FileFinder>, cx: &mut ViewContext<FileFinder>,
) -> Task<()> { ) -> Task<()> {
let relative_to = self.relative_to.clone(); let relative_to = self.relative_to.clone();
@ -183,7 +161,7 @@ impl FileFinderDelegate {
cx.spawn(|picker, mut cx| async move { cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets( let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(), candidate_sets.as_slice(),
query.path_query(), query.path_like.path_query(),
relative_to, relative_to,
false, false,
100, 100,
@ -206,18 +184,18 @@ impl FileFinderDelegate {
&mut self, &mut self,
search_id: usize, search_id: usize,
did_cancel: bool, did_cancel: bool,
query: FileSearchQuery, query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
cx: &mut ViewContext<FileFinder>, cx: &mut ViewContext<FileFinder>,
) { ) {
if search_id >= self.latest_search_id { if search_id >= self.latest_search_id {
self.latest_search_id = search_id; self.latest_search_id = search_id;
if self.latest_search_did_cancel if self.latest_search_did_cancel
&& Some(query.path_query()) && Some(query.path_like.path_query())
== self == self
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.map(|query| query.path_query()) .map(|query| query.path_like.path_query())
{ {
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
} else { } else {
@ -265,7 +243,19 @@ impl PickerDelegate for FileFinderDelegate {
cx.notify(); cx.notify();
Task::ready(()) Task::ready(())
} else { } else {
self.spawn_search(FileSearchQuery::new(raw_query), cx) let raw_query = &raw_query;
let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: raw_query.to_owned(),
file_query_end: if path_like_str == raw_query {
None
} else {
Some(path_like_str.len())
},
})
})
.expect("infallible");
self.spawn_search(query, cx)
} }
} }
@ -286,12 +276,12 @@ impl PickerDelegate for FileFinderDelegate {
let row = self let row = self
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.and_then(|query| query.file_row) .and_then(|query| query.row)
.map(|row| row.saturating_sub(1)); .map(|row| row.saturating_sub(1));
let col = self let col = self
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.and_then(|query| query.file_column) .and_then(|query| query.column)
.unwrap_or(0) .unwrap_or(0)
.saturating_sub(1); .saturating_sub(1);
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
@ -477,10 +467,13 @@ mod tests {
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.expect("Finder should have a query after the update_matches call"); .expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.raw_query, query_inside_file); assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
assert_eq!(latest_search_query.file_row, Some(file_row)); assert_eq!(
assert_eq!(latest_search_query.file_column, Some(file_column as u32)); latest_search_query.path_like.file_query_end,
assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
@ -564,10 +557,13 @@ mod tests {
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.expect("Finder should have a query after the update_matches call"); .expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.raw_query, query_outside_file); assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
assert_eq!(latest_search_query.file_row, Some(file_row)); assert_eq!(
assert_eq!(latest_search_query.file_column, Some(file_column as u32)); latest_search_query.path_like.file_query_end,
assert_eq!(latest_search_query.file_query_end, Some(file_query.len())); Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
@ -634,7 +630,7 @@ mod tests {
) )
}); });
let query = FileSearchQuery::new("hi".to_string()); let query = test_path_like("hi");
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
.await; .await;
@ -719,8 +715,7 @@ mod tests {
}); });
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("hi"), cx)
.spawn_search(FileSearchQuery::new("hi".to_string()), cx)
}) })
.await; .await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
@ -758,8 +753,7 @@ mod tests {
// is included in the matching, because the worktree is a single file. // is included in the matching, because the worktree is a single file.
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("thf"), cx)
.spawn_search(FileSearchQuery::new("thf".to_string()), cx)
}) })
.await; .await;
cx.read(|cx| { cx.read(|cx| {
@ -779,8 +773,7 @@ mod tests {
// not match anything. // not match anything.
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
.spawn_search(FileSearchQuery::new("thf/".to_string()), cx)
}) })
.await; .await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
@ -826,8 +819,7 @@ mod tests {
// Run a search that matches two files with the same relative path. // Run a search that matches two files with the same relative path.
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
.spawn_search(FileSearchQuery::new("a.t".to_string()), cx)
}) })
.await; .await;
@ -884,8 +876,7 @@ mod tests {
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
.spawn_search(FileSearchQuery::new("a.txt".to_string()), cx)
}) })
.await; .await;
@ -928,8 +919,7 @@ mod tests {
}); });
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut() f.delegate_mut().spawn_search(test_path_like("dir"), cx)
.spawn_search(FileSearchQuery::new("dir".to_string()), cx)
}) })
.await; .await;
cx.read(|cx| { cx.read(|cx| {
@ -937,4 +927,18 @@ mod tests {
assert_eq!(finder.delegate().matches.len(), 0); assert_eq!(finder.delegate().matches.len(), 0);
}); });
} }
fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
PathLikeWithPosition::parse_str(test_str, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: test_str.to_owned(),
file_query_end: if path_like_str == test_str {
None
} else {
Some(path_like_str.len())
},
})
})
.unwrap()
}
} }

View File

@ -16,3 +16,4 @@ settings = { path = "../settings" }
text = { path = "../text" } text = { path = "../text" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage.workspace = true postage.workspace = true
util = { path = "../util" }

View File

@ -1,8 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use editor::{ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor, FILE_ROW_COLUMN_DELIMITER,
};
use gpui::{ use gpui::{
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
View, ViewContext, ViewHandle, View, ViewContext, ViewHandle,
@ -10,6 +8,7 @@ use gpui::{
use menu::{Cancel, Confirm}; use menu::{Cancel, Confirm};
use settings::Settings; use settings::Settings;
use text::{Bias, Point}; use text::{Bias, Point};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, Workspace}; use workspace::{Modal, Workspace};
actions!(go_to_line, [Toggle]); actions!(go_to_line, [Toggle]);

View File

@ -70,3 +70,40 @@ pub fn compact(path: &Path) -> PathBuf {
path.to_path_buf() path.to_path_buf()
} }
} }
pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
#[derive(Debug, Clone)]
pub struct PathLikeWithPosition<P> {
pub path_like: P,
pub row: Option<u32>,
pub column: Option<u32>,
}
impl<P> PathLikeWithPosition<P> {
pub fn parse_str<F, E>(s: &str, parse_path_like_str: F) -> Result<Self, E>
where
F: Fn(&str) -> Result<P, E>,
{
let mut components = s.splitn(3, FILE_ROW_COLUMN_DELIMITER).map(str::trim).fuse();
let path_like_str = components.next().filter(|str| !str.is_empty());
let row = components.next().and_then(|row| row.parse::<u32>().ok());
let column = components
.next()
.filter(|_| row.is_some())
.and_then(|col| col.parse::<u32>().ok());
Ok(match path_like_str {
Some(path_like_str) => Self {
path_like: parse_path_like_str(path_like_str)?,
row,
column,
},
None => Self {
path_like: parse_path_like_str(s)?,
row: None,
column: None,
},
})
}
}