diff --git a/src/terminal/view.rs b/src/terminal/view.rs index ccc7cd50..5135c09a 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -263,6 +263,23 @@ fn cwd_is_on_host(pane_runs_remotely: bool, host_is_local: bool) -> bool { } } +/// The cwd a native SSH pane's remote shell reported, for the few readers that +/// only need a name for it and not a host to act on it. +/// +/// Such a pane belongs to this machine's daemon, so [`cwd_is_on_host`] rightly +/// turns its paths away from every `Host` call — there is no host to hand them +/// to. But the shell on the far end states them itself (OSC 7), unlike a shell +/// that ssh'd onward from a local prompt, whose directory is only ever a guess. +/// Only an absolute POSIX path counts: that is what a remote sshd's shell +/// reports, and anything else is not a directory worth naming. +fn native_ssh_cwd( + remote: Option<&RemoteContext>, + cwd: Option, +) -> Option { + remote.filter(|r| r.kind == crate::daemon::protocol::RemoteKind::NativeSsh)?; + cwd.filter(|c| c.to_string_lossy().starts_with('/')) +} + /// Which path dialect a pane's output is written in. /// /// A pane running on this machine spells paths the way this OS does, and that @@ -1950,6 +1967,12 @@ impl TerminalView { self.git_status_cwd.as_deref() } + /// See [`native_ssh_cwd`]. `None` for every pane that is not a native SSH + /// one — those either have a `git_status_cwd` or have no cwd to name. + pub fn native_ssh_cwd(&self) -> Option { + native_ssh_cwd(self.remote_context().as_ref(), self.cwd()) + } + /// Plant the cwd the git-status poll would have found. For tests that /// need a pane to look like it is sitting somewhere known — a real poll /// needs a live shell reporting a directory, which a quiet test pane has @@ -9754,6 +9777,37 @@ mod tests { assert!(!cwd_is_on_host(false, false)); } + #[test] + fn only_a_native_ssh_pane_names_its_remote_cwd() { + use super::{RemoteContext, native_ssh_cwd}; + use std::path::PathBuf; + let native = RemoteContext { + kind: RemoteKind::NativeSsh, + argv: Vec::new(), + target: "ubuntu@box".into(), + }; + let home = || Some(PathBuf::from("/home/ubuntu")); + assert_eq!(native_ssh_cwd(Some(&native), home()), home()); + + // `ssh` typed at a local prompt: the directory is a guess, not a + // report from the far end. + let typed = RemoteContext { + kind: RemoteKind::Ssh, + ..native.clone() + }; + assert_eq!(native_ssh_cwd(Some(&typed), home()), None); + assert_eq!(native_ssh_cwd(Some(&wsl_context("Ubuntu")), home()), None); + // A local pane has its own path through `git_status_cwd`. + assert_eq!(native_ssh_cwd(None, home()), None); + + assert_eq!(native_ssh_cwd(Some(&native), None), None); + assert_eq!( + native_ssh_cwd(Some(&native), Some(PathBuf::from("~"))), + None, + "only an absolute path names a directory" + ); + } + /// Which machine's spelling a pane's paths are read in. Ungated on /// purpose: the bug this settles was a Windows-only one that hid behind a /// `#[cfg(unix)]` on the test that covered it. diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index a95b80a4..98ac9174 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -1877,15 +1877,25 @@ impl Tty7App { if stated.as_ref().is_some_and(GroupKey::is_custom) { return stated; } - let cwd = tab.pane.first_leaf().and_then(|leaf| { + let resolved = tab.pane.first_leaf().and_then(|leaf| { let view = leaf.terminal()?.read(cx); - Some((view.host_id(), view.git_status_cwd()?.to_path_buf())) - }); - if let Some((id, cwd)) = cwd { - let known = cx.global::().known_repo_for(id, &cwd); - if let Some(group) = resolved_group(grouping, known, &cwd) { - *tab.sidebar_group.borrow_mut() = group; + match view.git_status_cwd() { + Some(cwd) => { + let known = cx + .global::() + .known_repo_for(view.host_id(), cwd); + resolved_group(grouping, known, cwd) + } + // A native SSH pane never gets a `git_status_cwd`: + // its paths are on a machine no `Host` reaches, so + // nothing probes them (#891). Its shell still says + // where it is, though, and that is enough for the + // folder fallback. + None => unprobed_group(grouping, &view.native_ssh_cwd()?), } + }); + if let Some(group) = resolved { + *tab.sidebar_group.borrow_mut() = group; } tab.sidebar_group.borrow().clone() }) @@ -1977,6 +1987,20 @@ fn resolved_group( }) } +/// The group for a cwd no repo probe can ever run in — a native SSH pane's. +/// "Never probed" would leave such a tab in Scratch for good, so it is read +/// as a settled "no repo": repo-or-directory grouping files it under the +/// folder, the same as a local shell in a plain directory, and repo grouping +/// leaves it in Scratch. A remote repo therefore groups by the folder the +/// shell is in, not its root — there is nothing to ask for the root. +/// +/// The key is the bare path, the same as every other derived group: a remote +/// `/home/ubuntu` and a local one share a header, as two remote workspaces' +/// identical paths already would. +fn unprobed_group(grouping: SidebarGrouping, cwd: &Path) -> Option> { + resolved_group(grouping, Some(None), cwd) +} + #[derive(Debug, PartialEq)] struct Section { key: Option, @@ -2697,6 +2721,24 @@ mod tests { ); } + #[test] + fn a_native_ssh_cwd_groups_by_its_remote_folder() { + let home = p("/home/ubuntu"); + assert_eq!( + unprobed_group(SidebarGrouping::RepoOrDirectory, &home), + Some(Some(g("/home/ubuntu"))) + ); + assert_eq!( + unprobed_group(SidebarGrouping::Repo, &home), + Some(None), + "under Repo there is no repo to find, so Scratch" + ); + // Named by the last segment, like a local folder group. + let keys = [Some(g("/home/ubuntu")), None]; + let sections = sidebar_sections(&keys); + assert_eq!(sections[0].name.as_deref(), Some("ubuntu")); + } + #[test] fn a_known_repo_home_wins_over_the_folder_in_both_modes() { for mode in [SidebarGrouping::Repo, SidebarGrouping::RepoOrDirectory] {