Cargo.lock π
@@ -8133,6 +8133,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
+ "collections",
"db",
"dirs 4.0.0",
"editor",
Robin PfΓ€ffle and Kirill created
Allow opening file paths relative to terminal's cwd
https://github.com/zed-industries/zed/assets/67913738/413a1107-541e-4c25-ae7c-cbe45469d452
Release Notes:
- Added support for opening file paths relative to terminal's cwd
([#7144](https://github.com/zed-industries/zed/issues/7144)).
---------
Co-authored-by: Kirill <kirill@zed.dev>
Cargo.lock | 1
crates/terminal/src/terminal.rs | 27 ++
crates/terminal_view/Cargo.toml | 1
crates/terminal_view/src/terminal_view.rs | 216 ++++++++++++++++--------
crates/util/src/paths.rs | 2
5 files changed, 168 insertions(+), 79 deletions(-)
@@ -8133,6 +8133,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
+ "collections",
"db",
"dirs 4.0.0",
"editor",
@@ -86,6 +86,15 @@ pub enum Event {
Open(MaybeNavigationTarget),
}
+#[derive(Clone, Debug)]
+pub struct PathLikeTarget {
+ /// File system path, absolute or relative, existing or not.
+ /// Might have line and column number(s) attached as `file.rs:1:23`
+ pub maybe_path: String,
+ /// Current working directory of the terminal
+ pub terminal_dir: Option<PathBuf>,
+}
+
/// A string inside terminal, potentially useful as a URI that can be opened.
#[derive(Clone, Debug)]
pub enum MaybeNavigationTarget {
@@ -93,7 +102,7 @@ pub enum MaybeNavigationTarget {
Url(String),
/// File system path, absolute or relative, existing or not.
/// Might have line and column number(s) attached as `file.rs:1:23`
- PathLike(String),
+ PathLike(PathLikeTarget),
}
#[derive(Clone)]
@@ -626,6 +635,12 @@ impl Terminal {
}
}
+ fn get_cwd(&self) -> Option<PathBuf> {
+ self.foreground_process_info
+ .as_ref()
+ .map(|info| info.cwd.clone())
+ }
+
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
@@ -800,7 +815,10 @@ impl Terminal {
let target = if is_url {
MaybeNavigationTarget::Url(maybe_url_or_path)
} else {
- MaybeNavigationTarget::PathLike(maybe_url_or_path)
+ MaybeNavigationTarget::PathLike(PathLikeTarget {
+ maybe_path: maybe_url_or_path,
+ terminal_dir: self.get_cwd(),
+ })
};
cx.emit(Event::Open(target));
} else {
@@ -852,7 +870,10 @@ impl Terminal {
let navigation_target = if is_url {
MaybeNavigationTarget::Url(word)
} else {
- MaybeNavigationTarget::PathLike(word)
+ MaybeNavigationTarget::PathLike(PathLikeTarget {
+ maybe_path: word,
+ terminal_dir: self.get_cwd(),
+ })
};
cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
}
@@ -12,6 +12,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
db = { path = "../db" }
+collections = { path = "../collections" }
dirs = "4.0.0"
editor = { path = "../editor" }
futures.workspace = true
@@ -2,7 +2,9 @@ mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
+use collections::HashSet;
use editor::{scroll::Autoscroll, Editor};
+use futures::{stream::FuturesUnordered, StreamExt};
use gpui::{
div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels,
@@ -10,7 +12,7 @@ use gpui::{
};
use language::Bias;
use persistence::TERMINAL_DB;
-use project::{search::SearchQuery, LocalWorktree, Project};
+use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
use terminal::{
alacritty_terminal::{
index::Point,
@@ -177,8 +179,21 @@ impl TerminalView {
Event::NewNavigationTarget(maybe_navigation_target) => {
this.can_navigate_to_selected_word = match maybe_navigation_target {
Some(MaybeNavigationTarget::Url(_)) => true,
- Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
- !possible_open_targets(&workspace, maybe_path, cx).is_empty()
+ Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
+ if let Ok(fs) = workspace.update(cx, |workspace, cx| {
+ workspace.project().read(cx).fs().clone()
+ }) {
+ let valid_files_to_open_task = possible_open_targets(
+ fs,
+ &workspace,
+ &path_like_target.terminal_dir,
+ &path_like_target.maybe_path,
+ cx,
+ );
+ smol::block_on(valid_files_to_open_task).len() > 0
+ } else {
+ false
+ }
}
None => false,
}
@@ -187,57 +202,60 @@ impl TerminalView {
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.open_url(url),
- MaybeNavigationTarget::PathLike(maybe_path) => {
+ MaybeNavigationTarget::PathLike(path_like_target) => {
if !this.can_navigate_to_selected_word {
return;
}
- let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
- if let Some(path) = potential_abs_paths.into_iter().next() {
- let task_workspace = workspace.clone();
- cx.spawn(|_, mut cx| async move {
- let fs = task_workspace.update(&mut cx, |workspace, cx| {
- workspace.project().read(cx).fs().clone()
- })?;
- let is_dir = fs
- .metadata(&path.path_like)
- .await?
- .with_context(|| {
- format!("Missing metadata for file {:?}", path.path_like)
- })?
- .is_dir;
- let opened_items = task_workspace
- .update(&mut cx, |workspace, cx| {
- workspace.open_paths(
- vec![path.path_like],
- OpenVisible::OnlyDirectories,
- None,
- cx,
- )
- })
- .context("workspace update")?
- .await;
- anyhow::ensure!(
- opened_items.len() == 1,
- "For a single path open, expected single opened item"
- );
- let opened_item = opened_items
- .into_iter()
- .next()
- .unwrap()
- .transpose()
- .context("path open")?;
- if is_dir {
- task_workspace.update(&mut cx, |workspace, cx| {
- workspace.project().update(cx, |_, cx| {
- cx.emit(project::Event::ActivateProjectPanel);
- })
- })?;
- } else {
+ let task_workspace = workspace.clone();
+ let Some(fs) = workspace
+ .update(cx, |workspace, cx| {
+ workspace.project().read(cx).fs().clone()
+ })
+ .ok()
+ else {
+ return;
+ };
+
+ let path_like_target = path_like_target.clone();
+ cx.spawn(|terminal_view, mut cx| async move {
+ let valid_files_to_open = terminal_view
+ .update(&mut cx, |_, cx| {
+ possible_open_targets(
+ fs,
+ &task_workspace,
+ &path_like_target.terminal_dir,
+ &path_like_target.maybe_path,
+ cx,
+ )
+ })?
+ .await;
+ let paths_to_open = valid_files_to_open
+ .iter()
+ .map(|(p, _)| p.path_like.clone())
+ .collect();
+ let opened_items = task_workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_paths(
+ paths_to_open,
+ OpenVisible::OnlyDirectories,
+ None,
+ cx,
+ )
+ })
+ .context("workspace update")?
+ .await;
+
+ let mut has_dirs = false;
+ for ((path, metadata), opened_item) in valid_files_to_open
+ .into_iter()
+ .zip(opened_items.into_iter())
+ {
+ if metadata.is_dir {
+ has_dirs = true;
+ } else if let Some(Ok(opened_item)) = opened_item {
if let Some(row) = path.row {
let col = path.column.unwrap_or(0);
- if let Some(active_editor) =
- opened_item.and_then(|item| item.downcast::<Editor>())
- {
+ if let Some(active_editor) = opened_item.downcast::<Editor>() {
active_editor
.downgrade()
.update(&mut cx, |editor, cx| {
@@ -259,10 +277,19 @@ impl TerminalView {
}
}
}
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
+ }
+
+ if has_dirs {
+ task_workspace.update(&mut cx, |workspace, cx| {
+ workspace.project().update(cx, |_, cx| {
+ cx.emit(project::Event::ActivateProjectPanel);
+ })
+ })?;
+ }
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx)
}
},
Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
@@ -554,48 +581,87 @@ impl TerminalView {
}
}
+fn possible_open_paths_metadata(
+ fs: Arc<dyn Fs>,
+ row: Option<u32>,
+ column: Option<u32>,
+ potential_paths: HashSet<PathBuf>,
+ cx: &mut ViewContext<TerminalView>,
+) -> Task<Vec<(PathLikeWithPosition<PathBuf>, Metadata)>> {
+ cx.background_executor().spawn(async move {
+ let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
+
+ let mut fetch_metadata_tasks = potential_paths
+ .into_iter()
+ .map(|potential_path| async {
+ let metadata = fs.metadata(&potential_path).await.ok().flatten();
+ (
+ PathLikeWithPosition {
+ path_like: potential_path,
+ row,
+ column,
+ },
+ metadata,
+ )
+ })
+ .collect::<FuturesUnordered<_>>();
+
+ while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
+ if let Some(metadata) = metadata {
+ paths_with_metadata.push((path, metadata));
+ }
+ }
+
+ paths_with_metadata
+ })
+}
+
fn possible_open_targets(
+ fs: Arc<dyn Fs>,
workspace: &WeakView<Workspace>,
+ cwd: &Option<PathBuf>,
maybe_path: &String,
- cx: &mut ViewContext<'_, TerminalView>,
-) -> Vec<PathLikeWithPosition<PathBuf>> {
+ cx: &mut ViewContext<TerminalView>,
+) -> Task<Vec<(PathLikeWithPosition<PathBuf>, Metadata)>> {
let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
})
.expect("infallible");
+ let row = path_like.row;
+ let column = path_like.column;
let maybe_path = path_like.path_like;
let potential_abs_paths = if maybe_path.is_absolute() {
- vec![maybe_path]
+ HashSet::from_iter([maybe_path])
} else if maybe_path.starts_with("~") {
if let Some(abs_path) = maybe_path
.strip_prefix("~")
.ok()
.and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
{
- vec![abs_path]
+ HashSet::from_iter([abs_path])
} else {
- Vec::new()
+ HashSet::default()
}
- } else if let Some(workspace) = workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- workspace
- .worktrees(cx)
- .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
- .collect()
- })
} else {
- Vec::new()
+ // First check cwd and then workspace
+ let mut potential_cwd_and_workspace_paths = HashSet::default();
+ if let Some(cwd) = cwd {
+ potential_cwd_and_workspace_paths.insert(Path::join(cwd, &maybe_path));
+ }
+ if let Some(workspace) = workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ for potential_worktree_path in workspace
+ .worktrees(cx)
+ .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
+ {
+ potential_cwd_and_workspace_paths.insert(potential_worktree_path);
+ }
+ });
+ }
+ potential_cwd_and_workspace_paths
};
- potential_abs_paths
- .into_iter()
- .filter(|path| path.exists())
- .map(|path| PathLikeWithPosition {
- path_like: path,
- row: path_like.row,
- column: path_like.column,
- })
- .collect()
+ possible_open_paths_metadata(fs, row, column, potential_abs_paths, cx)
}
pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
@@ -121,7 +121,7 @@ pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
/// A representation of a path-like string with optional row and column numbers.
/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct PathLikeWithPosition<P> {
pub path_like: P,
pub row: Option<u32>,