diff --git a/Cargo.lock b/Cargo.lock index 14b5882ff9..242e3d4704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8133,6 +8133,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "collections", "db", "dirs 4.0.0", "editor", diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 5bc8d00e52..ad9587a326 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -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, +} + /// 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 { + 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))); } diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index dfffe3824f..134f9f08dd 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -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 diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c0074cf53a..6c7270d9b4 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -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::()) - { + if let Some(active_editor) = opened_item.downcast::() { 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, + row: Option, + column: Option, + potential_paths: HashSet, + cx: &mut ViewContext, +) -> Task, 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::>(); + + 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, workspace: &WeakView, + cwd: &Option, maybe_path: &String, - cx: &mut ViewContext<'_, TerminalView>, -) -> Vec> { + cx: &mut ViewContext, +) -> Task, 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 { diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 6e7fc3f653..cd839ae50e 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -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

{ pub path_like: P, pub row: Option,