分屏时右侧栏跟着焦点所在的远程终端,标题写出主机

中间区分屏后,焦点点进哪个标签它就在前,状态栏和右侧栏跟着变;点进另一半的 SFTP、
本地终端时右侧栏仍对着刚才的远程终端,屏幕上没有远程终端才隐藏。拖动合并标签组后
重新核对谁在前,修好标签被盖住后右侧栏还留着的问题。右侧栏标题加上主机名。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-07 23:59:06 +08:00
1 parent 77da643999
commit 4d70be98a9
10 files changed
+407 -37

No files matched your search

+3 -1
View File
@@ -44,6 +44,7 @@ ShellRS(crate 与二进制都叫 `shellrs`)是 Xshell / WinSCP 式的 SSH
- **开始页**:没有标签时显示「最近连接」。「快速连接」搜索保存的主机、回车连接;多选做好但关着(常量 `MULTIPLE`)。
- **右侧栏**:窗口最右一列竖排的工具切换。
- 显示规则:只跟着 SSH 远程终端出现,SFTP、本地终端、设置、开始页都不显示。它是工作区级的一份,开合和所选工具在所有终端间共用。默认收起,宽 320 px,只能往宽里拉。⌘⌥B / Ctrl+Alt+B 切换。
- 中间区分屏时不隐藏:跟着最近一次在前(切到或焦点点进)、而且还在屏幕上的远程终端;点进另一半的 SFTP、本地终端等不换;屏幕上一个远程终端都没有才隐藏。标题写出主机(「系统监控 · web-01」),状态栏也跟焦点走。用户比较过「分屏就隐藏」,选了这个。
- 工具从上到下:命令片段、历史命令、Docker、系统服务、进程管理、网络连接、系统监控。
- 已知不是 Linux 的主机不显示系统监控、网络连接、进程管理、系统服务;已知是 Windows 的再去掉 Docker 和历史命令;外部连接只有命令片段。
- 系统监控每 2 秒读一次(运行时长随读数刷新,不自己走),磁盘每 30 秒,只在显示时读。≥ 70% 黄、≥ 90% 红。
@@ -249,7 +250,8 @@ cargo test -- --ignored # 会读写真实钥匙串的测试
- 显示按键别写死文字,用 `tooltip_with_action` / `Kbd` 从 keymap 取,否则改绑后会过期。
- **标签页全部可关。** 关闭走 `DockArea::remove_panel`(`TabGroup::close_panel` 拒绝关最后一个)。中间区面板的 `closable()` 返回 `false`,否则「…」菜单会多一个绕过确认的「关闭」。标题用 `ClosableTabTitle`,它带标签菜单,双击切换侧栏。`panel_name()` 是持久化键,不能改。
- **左右侧栏各是一个面板。** 左侧 Dock 里只有 `Sidebar`(三个列表),右侧 Dock 里只有 `ToolSidebar`。不用 `set_dock` 换面板,否则焦点会丢。
- 当前标签只经 `Workspace::set_active_tab` 改,否则右侧栏不跟着变。
- 当前标签只经 `Workspace::set_active_tab` 改,否则右侧栏不跟着变。分屏时焦点决定谁在前:每个中间区标签创建时都要用 `front::follow_focus` 登记焦点(它在焦点阶段触发,处理要 `defer_in`);布局变化(拖动合并标签组)后 `sync_front` 重新核对。右侧栏跟的终端由 `visible_center_tabs`(各组显示的标签,放大时只算放大的那组)判断是否还在屏幕上。
- 测试里拆分、合并标签组用 `DockArea::move_panel` 加 `InsertTarget::Split` / `Tabs`,同拖放;`split_at` 不会把面板从原组移走。
- 右侧 Dock 不可折叠,否则标签栏会多出 gpui-kit 的折叠按钮;关闭时临时设成可折叠。
- 最窄 320 px,由 `hold_tool_sidebar_width` 维持。
- **动画用 gpui-kit 的 `animate_keyframes`**(转发示意图),不自己写播放。
+1 -1
View File
@@ -203,7 +203,7 @@ The terminal’s context menu has “Copy”, “Paste”, “Find…” and “
## Right sidebar
When the current tab is a remote terminal, a column of tool buttons runs down the right edge of the window: Snippets, History, Docker, Services, Processes, Network and Monitor. Click one to open that tool on the right, and click it again to collapse it; ⌘⌥B (Ctrl+Alt+B on Windows and Linux) shows or hides the right sidebar. Drag the right sidebar’s left edge to widen it; it can’t get narrower than it was when it opened. The right sidebar appears only with remote terminals: switching to SFTP, a local terminal or Settings hides it together with its buttons, and going back to any remote terminal brings it back as it was, now for the host of the current terminal. Hosts known not to run Linux (macOS, Windows, BSD) don’t show the Services, Processes, Network and Monitor icons; History and Docker are there for every host except Windows ones (on a Mac, Docker works too if Docker Desktop is installed).
When the current tab is a remote terminal, a column of tool buttons runs down the right edge of the window: Snippets, History, Docker, Services, Processes, Network and Monitor. Click one to open that tool on the right, and click it again to collapse it; ⌘⌥B (Ctrl+Alt+B on Windows and Linux) shows or hides the right sidebar. Drag the right sidebar’s left edge to widen it; it can’t get narrower than it was when it opened. The right sidebar appears only with remote terminals: switching to SFTP, a local terminal or Settings hides it together with its buttons, and going back to any remote terminal brings it back as it was, now for the host of the current terminal. With tabs dragged into a split, the right sidebar stays: it works on the remote terminal you switched to or clicked into last, stays put when you click into SFTP or a local terminal in the other half, and hides only when no remote terminal is on screen. Its title names the host it works on (such as “Monitor · web-01”), and the status bar follows the half you clicked into too. Hosts known not to run Linux (macOS, Windows, BSD) don’t show the Services, Processes, Network and Monitor icons; History and Docker are there for every host except Windows ones (on a Mac, Docker works too if Docker Desktop is installed).
**Snippets** keeps the commands you use often, shared by every host. Click the plus at the top to create a snippet: a name, a category and a command, which can span several lines. Snippets are grouped by category, with the ones without a category last, under “Uncategorized”; click a category heading to collapse or expand it, and right-click it to create a snippet in that category, rename the category or delete it (its snippets are deleted with it, and the confirmation says how many). The folder button at the top creates a category. You can search by name or command; with several words, snippets that contain all of them are listed.
+1 -1
View File
@@ -203,7 +203,7 @@ ShellRS /sessionname=标签名 sftp://用户[:密码]@地址[:端口]
## 右侧栏
当前标签是远程终端时,窗口最右边有一列工具按钮:命令片段、历史命令、Docker、系统服务、进程管理、网络连接、系统监控。点一个就在右侧打开这个工具,再点一次收起;⌘⌥B(其他平台 Ctrl+Alt+B)显示或隐藏右侧栏。拖动右侧栏的左边缘可以加宽,最窄就是打开时的宽度。右侧栏只跟着远程终端出现,切到 SFTP、本地终端或设置时连同按钮一起隐藏,回到任一远程终端时按原样恢复,内容对应当前这个终端的主机。已知不是 Linux 的主机(macOS、Windows、BSD)不显示系统服务、进程管理、网络连接和系统监控的图标;历史命令和 Docker 除了 Windows 主机都有(Mac 上装了 Docker Desktop 也能用)。
当前标签是远程终端时,窗口最右边有一列工具按钮:命令片段、历史命令、Docker、系统服务、进程管理、网络连接、系统监控。点一个就在右侧打开这个工具,再点一次收起;⌘⌥B(其他平台 Ctrl+Alt+B)显示或隐藏右侧栏。拖动右侧栏的左边缘可以加宽,最窄就是打开时的宽度。右侧栏只跟着远程终端出现,切到 SFTP、本地终端或设置时连同按钮一起隐藏,回到任一远程终端时按原样恢复,内容对应当前这个终端的主机。把标签拖成分屏后,右侧栏不隐藏:它对应最近一次切到或点进去的远程终端,点进另一半的 SFTP 或本地终端时不变,屏幕上一个远程终端都没有时才隐藏;标题里写着它对应哪台主机(如「系统监控 · web-01」),状态栏也跟着点进去的那一半。已知不是 Linux 的主机(macOS、Windows、BSD)不显示系统服务、进程管理、网络连接和系统监控的图标;历史命令和 Docker 除了 Windows 主机都有(Mac 上装了 Docker Desktop 也能用)。
**命令片段**保存常用的命令,所有主机共用。点顶部的加号新建片段:名称、分类和命令,命令可以有多行。片段按分类分组显示,没有分类的放在最后的「未分类」下;点分类标题折叠或展开,右键分类标题可以在这个分类里新建片段、重命名或删除分类(分类里的片段会一起删除,确认时写明个数);顶部的文件夹按钮新建分类。可以按名称或命令搜索,输入几个词时列出同时包含这些词的片段。
+4
View File
@@ -8,6 +8,7 @@ use gpui_kit::component::dock::{DockPlacement, panel_handle};
use gpui_kit::*;
use super::Workspace;
use super::front::follow_focus;
use crate::analytics::Counter;
use crate::app::{CenterTab, CloseEditor, EditorAction, EditorCommand, EditorShortcut};
use crate::editor::{EditorId, EditorKey, EditorPanel, EditorPanelEvent, EditorSource};
@@ -95,6 +96,9 @@ impl Workspace {
cx.new(|cx| EditorPanel::new(id, source, location, file, store, dispatch, window, cx));
let subscription = cx.subscribe_in(&editor, window, Self::on_editor_event);
self._subscriptions.push(subscription);
let focus = editor.read(cx).focus_handle(cx);
self._subscriptions
.push(follow_focus(CenterTab::Editor(id), &focus, window, cx));
self.editors.insert(id, editor.clone());
self.dock_area.update(cx, |area, cx| {
area.add_panel_view(
+108
View File
@@ -0,0 +1,108 @@
//! Which center tab is in front. Without a split it is the tab shown; with
//! the center split, several show at once and the one in front is the one
//! the user works in: the focus decides, not only a tab switch. The status
//! bar and the right sidebar follow it.
use gpui_kit::component::dock::{DockPlacement, PaneRef, PanelId};
use gpui_kit::*;
use crate::app::CenterTab;
use super::Workspace;
impl Workspace {
/// The center tabs on screen: the tab each group shows, or only the
/// zoomed group's.
pub(super) fn visible_center_tabs(&self, cx: &App) -> Vec<CenterTab> {
let area = self.dock_area.read(cx);
let Some(tree) = area.layout(DockPlacement::Center) else {
return Vec::new();
};
let zoomed = area.zoomed_group();
let mut shown: Vec<PanelId> = Vec::new();
tree.root().walk(&mut |node| {
if let PaneRef::Tabs { panels, active_ix } = node.kind()
&& zoomed.is_none_or(|zoomed| zoomed == node.id())
&& let Some(panel) = panels.get(active_ix).or(panels.last())
{
shown.push(*panel);
}
});
shown
.into_iter()
.filter_map(|panel| self.center_tab_for_panel(panel))
.collect()
}
/// The focus went into `tab`: in a split, clicking into the other half
/// brings that tab to the front without any tab being switched to.
/// Does what the tab's own activation does.
pub(super) fn focus_entered(
&mut self,
tab: CenterTab,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.active_tab == Some(tab) {
return;
}
// The host list marks the host of the tab in front.
let host = match tab {
CenterTab::Terminal(id) => match self.terminals.get(&id) {
Some(panel) => Some(panel.read(cx).host_id()),
None => return,
},
CenterTab::Explorer(id) => match self.explorers.get(&id) {
Some(panel) => Some(panel.read(cx).host_id()),
None => return,
},
CenterTab::Editor(id) => match self.editors.get(&id) {
Some(editor) => editor.read(cx).host_id(),
None => return,
},
CenterTab::LocalTerminal(_) | CenterTab::Settings => None,
};
self.store
.update(cx, |store, cx| store.set_active(host, cx));
self.set_active_tab(Some(tab), window, cx);
cx.notify();
}
/// After the center's layout changed (a tab dragged into another group,
/// a group closed): a tab in front that no longer shows gives way to the
/// one with the focus, or to one that shows; then the right sidebar
/// follows.
pub(super) fn sync_front(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let visible = self.visible_center_tabs(cx);
if let Some(tab) = self.active_tab
&& !visible.contains(&tab)
{
self.active_tab = visible
.iter()
.copied()
.find(|tab| {
self.tab_focus_handle(*tab, cx)
.is_some_and(|focus| focus.contains_focused(window, cx))
})
.or_else(|| visible.first().copied());
}
self.sync_tool_sidebar(window, cx);
}
}
/// Bring `tab` to the front whenever the focus goes into it. Registered with
/// every center tab, held for as long as the workspace.
pub(super) fn follow_focus(
tab: CenterTab,
focus: &FocusHandle,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Subscription {
cx.on_focus_in(focus, window, move |_, window, cx| {
// Focus listeners run in the frame's focus phase, where a change
// would not be drawn until something else asks for a frame.
cx.defer_in(window, move |this, window, cx| {
this.focus_entered(tab, window, cx)
});
})
}
+1
View File
@@ -8,6 +8,7 @@ mod credentials;
mod dock_skin;
mod editors;
mod forwards;
mod front;
mod links;
mod notices;
mod recent_hosts;
+32 -2
View File
@@ -110,6 +110,8 @@ pub struct ToolSidebar {
terminal: Option<ToolTerminal>,
/// Whether the right dock is open.
shown: bool,
/// For the name of the terminal's host, beside the tool's.
store: Entity<HostStore>,
monitor: Entity<MonitorPanel>,
snippets: Entity<SnippetPanel>,
history: Entity<HistoryPanel>,
@@ -133,6 +135,7 @@ impl ToolSidebar {
tool: ToolKind::default(),
terminal: None,
shown: false,
store: store.clone(),
monitor: cx.new(|_| MonitorPanel::new(dispatch.clone())),
snippets: cx.new(|cx| SnippetPanel::new(store, dispatch.clone(), window, cx)),
history: cx.new(|cx| HistoryPanel::new(dispatch.clone(), window, cx)),
@@ -327,11 +330,38 @@ impl BasePanel for ToolSidebar {
}
impl Panel for ToolSidebar {
fn title(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
/// The tool, and the host it works on: with the center split, more than
/// one SSH terminal may show.
fn title(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let host = self.terminal.as_ref().and_then(|terminal| {
self.store
.read(cx)
.host(terminal.host)
.map(|host| host.name.clone())
});
let muted = cx.theme().muted_foreground;
h_flex()
.id("tool-sidebar-title")
.test_support()
.min_w_0()
.gap_1()
.child(self.tool.icon().small())
.child(self.tool.label())
.child(div().flex_shrink_0().child(self.tool.label()))
.when_some(host, |title, host| {
title
.aria_label(format!("{} · {host}", self.tool.label()))
.child(div().flex_shrink_0().text_color(muted).child("·"))
.child(
div()
.id("tool-sidebar-host")
.test_support()
.aria_label(host.clone())
.min_w_0()
.truncate()
.text_color(muted)
.child(host),
)
})
}
fn zoom_control(&self, _: &App) -> Option<PanelControl> {
+18 -5
View File
@@ -3,7 +3,9 @@
//!
//! The sidebar goes with SSH terminals: with an SFTP tab, a local terminal,
//! the settings or the start page in front it is not there at all, switch
//! included. Whether it is open and which tool it shows stay as they were,
//! included. With the center split it works on the SSH terminal in front
//! last, as long as that one is on screen, and names its host; it hides once
//! no SSH terminal shows. Whether it is open and which tool it shows stay as they were,
//! so the next SSH terminal brings it back the same. A tool that cannot work
//! on the host in front (the monitor on a host known not to run Linux) is
//! not offered there: no button, and no sidebar while it is the one picked.
@@ -45,11 +47,19 @@ use super::{
pub(super) const TOOL_SIDEBAR_WIDTH: Pixels = px(320.);
impl Workspace {
/// The SSH terminal in front, which the right sidebar works on.
/// The SSH terminal the right sidebar works on: the one in front last
/// while it is on screen, else another that is.
pub(super) fn tool_terminal(&self, cx: &App) -> Option<ToolTerminal> {
let CenterTab::Terminal(id) = self.active_tab? else {
return None;
};
let visible = self.visible_center_tabs(cx);
let id = self
.tool_terminal_choice
.filter(|id| visible.contains(&CenterTab::Terminal(*id)))
.or_else(|| {
visible.iter().find_map(|tab| match tab {
CenterTab::Terminal(id) => Some(*id),
_ => None,
})
})?;
let panel = self.terminals.get(&id)?.read(cx);
Some(ToolTerminal {
id,
@@ -91,6 +101,9 @@ impl Workspace {
cx: &mut Context<Self>,
) {
self.active_tab = tab;
if let Some(CenterTab::Terminal(id)) = tab {
self.tool_terminal_choice = Some(id);
}
self.sync_tool_sidebar(window, cx);
}
+74 -27
View File
@@ -20,6 +20,7 @@ use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use super::editors::describe_files;
use super::front::follow_focus;
use crate::analytics::{Analytics, AnalyticsServices, Counter};
use crate::app::ExplorerDispatch as _;
use crate::app::{
@@ -187,10 +188,15 @@ pub struct Workspace {
next_remote_terminal_id: u64,
next_local_terminal_id: u64,
next_explorer_id: u64,
/// The center tab displayed most recently; `CloseActiveTab` closes it.
/// Changed through `set_active_tab`, which shows or hides the right
/// sidebar with it.
/// The center tab in front: shown most recently, or, with the center
/// split, the one the focus went into last (see `front.rs`).
/// `CloseActiveTab` closes it. Changed through `set_active_tab`, which
/// brings the right sidebar in line.
pub(super) active_tab: Option<CenterTab>,
/// The SSH terminal the right sidebar works on while it shows: the last
/// one in front. Another tab in front of the other half of a split
/// leaves it be.
pub(super) tool_terminal_choice: Option<RemoteTerminalId>,
/// Whether the right sidebar is to show while an SSH terminal is in
/// front. It hides with any other tab and comes back with the next one.
pub(super) tool_sidebar_wanted: bool,
@@ -472,6 +478,13 @@ impl Workspace {
);
center = center.panel_view(panel_handle(panel.clone()), cx);
subscriptions.push(subscription);
let focus = panel.read(cx).focus_handle(cx);
subscriptions.push(follow_focus(
CenterTab::Terminal(terminal_id),
&focus,
window,
cx,
));
terminals.insert(terminal_id, panel);
}
@@ -535,6 +548,7 @@ impl Workspace {
next_local_terminal_id: 1,
next_explorer_id: 1,
active_tab: None,
tool_terminal_choice: None,
tool_sidebar_wanted: false,
prompt_queue: VecDeque::new(),
active_prompt: None,
@@ -553,6 +567,11 @@ impl Workspace {
}
/// The host store, for tests and for panels created later.
/// The dock area: the center's tabs and the two sidebars.
pub fn dock_area(&self) -> &Entity<DockArea> {
&self.dock_area
}
pub fn store(&self) -> &Entity<HostStore> {
&self.store
}
@@ -643,29 +662,9 @@ impl Workspace {
/// Give the focus to the center: the tab shown last, or the start page
/// while there is none. For when the focus is about to go off screen.
pub(super) fn focus_center(&self, window: &mut Window, cx: &mut Context<Self>) {
let tab = match self.active_tab {
Some(CenterTab::Terminal(id)) => self
.terminals
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
Some(CenterTab::Explorer(id)) => self
.explorers
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
Some(CenterTab::LocalTerminal(id)) => self
.local_terminals
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
Some(CenterTab::Settings) => self
.settings_tab
.as_ref()
.map(|panel| panel.read(cx).focus_handle(cx)),
Some(CenterTab::Editor(id)) => self
.editors
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
None => None,
};
let tab = self
.active_tab
.and_then(|tab| self.tab_focus_handle(tab, cx));
let handle = tab.or_else(|| {
self.skin
.is_center_empty()
@@ -676,6 +675,32 @@ impl Workspace {
}
}
/// Where the focus goes in `tab`.
pub(super) fn tab_focus_handle(&self, tab: CenterTab, cx: &App) -> Option<FocusHandle> {
match tab {
CenterTab::Terminal(id) => self
.terminals
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
CenterTab::Explorer(id) => self
.explorers
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
CenterTab::LocalTerminal(id) => self
.local_terminals
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
CenterTab::Settings => self
.settings_tab
.as_ref()
.map(|panel| panel.read(cx).focus_handle(cx)),
CenterTab::Editor(id) => self
.editors
.get(&id)
.map(|panel| panel.read(cx).focus_handle(cx)),
}
}
/// Keep the start page and focus in step with the center: once its last
/// tab closes, the page fills it and takes the focus the tab held, so
/// window-level shortcuts keep a dispatch path.
@@ -689,6 +714,8 @@ impl Workspace {
let page = self.recent.read(cx).focus_handle(cx);
window.focus(&page, cx);
}
// A tab dragged into another group may hide the one in front.
self.sync_front(window, cx);
}
pub(super) fn enqueue_prompt(
@@ -1081,6 +1108,13 @@ impl Workspace {
cx,
);
self._subscriptions.push(subscription);
let focus = panel.read(cx).focus_handle(cx);
self._subscriptions.push(follow_focus(
CenterTab::Terminal(terminal_id),
&focus,
window,
cx,
));
self.terminals.insert(terminal_id, panel.clone());
self.refresh_host_connection_state(host_id, cx);
self.dock_area.update(cx, |area, cx| {
@@ -1112,6 +1146,9 @@ impl Workspace {
self.next_explorer_id += 1;
let (panel, subscription) = new_explorer_panel(self, id, host_id, window, cx);
self._subscriptions.push(subscription);
let focus = panel.read(cx).focus_handle(cx);
self._subscriptions
.push(follow_focus(CenterTab::Explorer(id), &focus, window, cx));
self.explorers.insert(id, panel.clone());
self.dock_area.update(cx, |area, cx| {
area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
@@ -1129,6 +1166,13 @@ impl Workspace {
let (panel, subscription) =
new_local_terminal_panel(id, self.local_terminal_factory.clone(), window, cx);
self._subscriptions.push(subscription);
let focus = panel.read(cx).focus_handle(cx);
self._subscriptions.push(follow_focus(
CenterTab::LocalTerminal(id),
&focus,
window,
cx,
));
self.local_terminals.insert(id, panel.clone());
self.count(Counter::LocalTerminal);
self.dock_area.update(cx, |area, cx| {
@@ -1174,6 +1218,9 @@ impl Workspace {
},
);
self._subscriptions.push(subscription);
let focus = panel.read(cx).focus_handle(cx);
self._subscriptions
.push(follow_focus(CenterTab::Settings, &focus, window, cx));
self.settings_tab = Some(panel.clone());
self.dock_area.update(cx, |area, cx| {
area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
@@ -1564,7 +1611,7 @@ impl Workspace {
}
/// The center tab a dock panel stands for, if it is one of ours.
fn center_tab_for_panel(&self, panel: PanelId) -> Option<CenterTab> {
pub(super) fn center_tab_for_panel(&self, panel: PanelId) -> Option<CenterTab> {
let is = |entity: EntityId| PanelId::from(entity) == panel;
self.terminals
.iter()
+165
View File
@@ -2,6 +2,10 @@
//! the title bar and dialogs in general.
use crate::support::*;
use gpui_kit::component::{
Placement,
dock::{DockPlacement, InsertTarget, PanelId},
};
#[gpui_kit::test]
fn tab_close_button_closes_the_terminal_and_disconnects(cx: &mut TestAppContext) {
@@ -983,6 +987,167 @@ async fn the_tool_sidebar_goes_with_ssh_terminals_only(cx: &mut TestAppContext)
});
}
/// With the center split, two SSH terminals show at once: the sidebar works
/// on the one the focus went into last, and names its host, as does the
/// status bar.
#[gpui_kit::test]
fn with_the_center_split_the_tool_sidebar_follows_the_focus(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace(cx);
in_frame(cx, handle, |window, cx| {
window.activate_window();
window.click(("terminal-tab", INITIAL_WEB_TERMINAL), cx);
});
in_frame(cx, handle, |window, cx| window.click("tool-snippets", cx));
in_frame(cx, handle, |window, _| {
assert_eq!(window.find("tool-sidebar-host").label(), Some("web-01"));
});
// staging-api's terminal to a group of its own on the right.
cx.update_window(handle.into(), |_, window, cx| {
let panel = PanelId::from(
workspace
.read(cx)
.remote_terminal(RemoteTerminalId(INITIAL_STAGING_TERMINAL))
.expect("staging-api's terminal")
.entity_id(),
);
let area = workspace.read(cx).dock_area().clone();
area.update(cx, |area, cx| {
let node = area
.layout(DockPlacement::Center)
.and_then(|tree| tree.find_panel_node(panel))
.expect("in the center");
// As a drag to the group's right edge does.
area.move_panel(
panel,
InsertTarget::Split {
node,
placement: Placement::Right,
size: None,
},
window,
cx,
);
});
})
.unwrap();
cx.run_until_parked();
let front = |cx: &mut TestAppContext| {
in_frame(cx, handle, |window, _| {
assert!(window.find(("terminal", INITIAL_WEB_TERMINAL)).visible());
assert!(
window
.find(("terminal", INITIAL_STAGING_TERMINAL))
.visible()
);
(
window.find("tool-sidebar-host").label().map(str::to_string),
window.find("status-connection").label().map(str::to_string),
)
})
};
// A click into either half, no tab switched to, brings it in front.
for (terminal, host) in [
(INITIAL_WEB_TERMINAL, "web-01"),
(INITIAL_STAGING_TERMINAL, "staging-api"),
(INITIAL_WEB_TERMINAL, "web-01"),
] {
in_frame(cx, handle, |window, cx| {
window.click(("terminal", terminal), cx)
});
let (sidebar, status) = front(cx);
assert_eq!(sidebar.as_deref(), Some(host));
// 「连接中 web-01」 until the fake connects: the host is what counts.
assert!(status.is_some_and(|status| status.ends_with(&format!(" {host}"))));
}
}
/// A tab dragged over the terminal's group, shown in front of it, takes the
/// sidebar away with the terminal: neither is on screen any more.
#[gpui_kit::test]
async fn a_tab_dropped_over_the_terminal_takes_the_sidebar_with_it(cx: &mut TestAppContext) {
let (handle, workspace) = open_workspace_with_sftp(cx, Arc::new(FakeSftpProvider::default()));
cx.run_until_parked();
in_frame(cx, handle, |window, cx| {
window.click(("terminal-tab", INITIAL_WEB_TERMINAL), cx)
});
in_frame(cx, handle, |window, cx| window.click("tool-snippets", cx));
open_test_explorer(cx, handle).await;
let explorer = cx.update(|cx| {
PanelId::from(
workspace
.read(cx)
.explorer(ExplorerId(SFTP_TAB))
.expect("the SFTP tab")
.entity_id(),
)
});
let terminal = cx.update(|cx| {
PanelId::from(
workspace
.read(cx)
.remote_terminal(RemoteTerminalId(INITIAL_WEB_TERMINAL))
.expect("web-01's terminal")
.entity_id(),
)
});
let move_explorer = |into_group: bool, cx: &mut TestAppContext| {
cx.update_window(handle.into(), |_, window, cx| {
let area = workspace.read(cx).dock_area().clone();
area.update(cx, |area, cx| {
let tree = area.layout(DockPlacement::Center).expect("the center");
let node = tree.find_panel_node(explorer).expect("in the center");
if into_group {
let target = tree.find_panel_node(terminal).expect("the terminals");
area.move_panel(
explorer,
InsertTarget::Tabs {
node: target,
ix: None,
activate: true,
},
window,
cx,
);
} else {
area.move_panel(
explorer,
InsertTarget::Split {
node,
placement: Placement::Bottom,
size: None,
},
window,
cx,
);
}
});
})
.unwrap();
cx.run_until_parked();
};
let shown = |cx: &mut TestAppContext| {
in_frame(cx, handle, |window, _| {
(
window.try_find("tool-switch").is_some(),
window.try_find("tool-sidebar-host").is_some(),
)
})
};
// Below the terminals: a terminal shows, and so do the tools.
move_explorer(false, cx);
assert_eq!(shown(cx), (true, true));
// Back into their group, in front, as a drop leaves it: no terminal
// shows.
move_explorer(true, cx);
assert_eq!(shown(cx), (false, false));
let front = cx.update(|cx| workspace.read(cx).active_tab());
assert_eq!(front, Some(CenterTab::Explorer(ExplorerId(SFTP_TAB))));
}
#[gpui_kit::test]
fn the_start_page_has_no_tool_switch(cx: &mut TestAppContext) {
let (handle, _) = open_workspace_with_store(cx, HostStore::empty());