From f1ac434ebf15038cdc9ca9ef3c350178c88baf5d Mon Sep 17 00:00:00 2001 From: Xuz Date: Tue, 6 Oct 2026 09:29:05 +0800 Subject: [PATCH] =?UTF-8?q?SFTP=20=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=EF=BC=9A=E5=8E=BB=E6=8E=89=E7=9B=AE=E5=BD=95=E7=9A=84=E3=80=8C?= =?UTF-8?q?=E6=89=93=E5=BC=80=E3=80=8D=EF=BC=8C=E5=8A=A0=E3=80=8C=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E8=B7=AF=E5=BE=84=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 文件名上的菜单不再有「打开」:目录双击就进入。「上传… / 下载…」下面 加「复制路径」,复制选中项目的完整路径,多个时每行一个,「..」是上级目录。 Co-Authored-By: Claude Opus 5.5 --- docs/manual.md | 2 +- src/app/actions.rs | 5 +++++ src/explorer/explorer_panel.rs | 20 ++++++++++++++++++++ src/explorer/file_pane.rs | 1 - src/explorer/pane_menu.rs | 18 ++++++++---------- tests/ui/sftp.rs | 28 ++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/docs/manual.md b/docs/manual.md index 0389676..25a934e 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -265,7 +265,7 @@ bash 在 shell 退出时才把这次执行的命令写进历史文件,所以 - 框选:在名称右边的空白、大小、类型、时间等列或列表下方按下拖动,画出选框,框住的行被选中;按住 ⌘ 或 Shift 开始拖动则加到已有选择上。拖到列表上下边缘时自动滚动。拖动文件名才是把文件拖到另一侧传输:按下未选中的文件名时只选中它(其他选择取消),拖走的就是它一个;按下已选中的文件名则拖走全部已选。拖动时只有到了另一侧列表上才显示「上传 / 下载 N 个项目」,在自己的列表或其他不能放下的地方,指针显示为禁止。 - 键盘:↑↓、Home、End、PageUp、PageDown 移动,加 Shift 扩展;空格或 Insert 切换当前行;⌘A 全选;Tab 切到另一侧。 - 右键未选中的名称格会先选中它;右键空白处取消选择并弹出空白处菜单。 - - 文件名上的菜单:打开(目录)或编辑(文件;图片、SVG 和 Markdown 另有预览,SVG 以外的图片只有预览)、上传… / 下载…、删除、重命名…、属性…。 + - 文件名上的菜单:编辑(文件;图片、SVG 和 Markdown 另有预览,SVG 以外的图片只有预览)、上传… / 下载…、复制路径(选中项目的完整路径,多个时每行一个)、删除、重命名…、属性…。目录双击进入,菜单里没有「打开」。 - 空白处的菜单:前往(上级 / 根 / 主目录 / 后退 / 前进)、刷新、添加路径到书签、新建。 ### 快捷键 diff --git a/src/app/actions.rs b/src/app/actions.rs index b273ae1..46e138d 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -457,9 +457,14 @@ pub enum ExplorerCommand { OpenDirectory { remote: bool, }, + /// 复制路径 on the path label: the directory shown. CopyPath { remote: bool, }, + /// 复制路径 on a row: the selected rows' full paths, one per line. + CopySelectedPaths { + remote: bool, + }, /// Move keyboard focus to a pane's file list. FocusPane { remote: bool, diff --git a/src/explorer/explorer_panel.rs b/src/explorer/explorer_panel.rs index 3e58d78..0d5518e 100644 --- a/src/explorer/explorer_panel.rs +++ b/src/explorer/explorer_panel.rs @@ -704,6 +704,23 @@ impl ExplorerPanel { cx.write_to_clipboard(ClipboardItem::new_string(path)); } } + ExplorerCommand::CopySelectedPaths { remote } => { + let pane = self.pane(*remote).read(cx); + let paths: Vec = pane + .selected_entries(cx) + .iter() + .map(|entry| { + if entry.is_parent() { + pane.parent_path() + } else { + pane.child_path_of(&entry.name) + } + }) + .collect(); + if !paths.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string(paths.join("\n"))); + } + } ExplorerCommand::FocusPane { remote } => { let focus = self.pane(*remote).read(cx).focus_handle(cx); window.focus(&focus, cx); @@ -1177,6 +1194,9 @@ mod tests { assert!(!needs_connection(&ExplorerCommand::CopyPath { remote: true })); + assert!(!needs_connection(&ExplorerCommand::CopySelectedPaths { + remote: true + })); assert!(!needs_connection(&ExplorerCommand::AddBookmark { remote: true, path: None diff --git a/src/explorer/file_pane.rs b/src/explorer/file_pane.rs index 4fcf636..5ea5c5a 100644 --- a/src/explorer/file_pane.rs +++ b/src/explorer/file_pane.rs @@ -930,7 +930,6 @@ impl FilePane { PaneMenuState { remote: self.is_remote(), explorer: self.explorer, - opens_directory: entries.len() == 1 && entries[0].is_dir(), edits_file: match entries.as_slice() { [entry] if !entry.is_dir() && !entry.is_parent() => { Some(self.child_path_of(&entry.name)) diff --git a/src/explorer/pane_menu.rs b/src/explorer/pane_menu.rs index a55a487..6a03ca1 100644 --- a/src/explorer/pane_menu.rs +++ b/src/explorer/pane_menu.rs @@ -14,8 +14,6 @@ pub(super) struct PaneMenuState { pub explorer: ExplorerId, /// The rows the item commands act on (the selection). pub targets: Vec, - /// The single selected row is a directory (or a link to one). - pub opens_directory: bool, /// The single selected row is a file the editor can try to open: its /// full path. pub edits_file: Option, @@ -43,14 +41,8 @@ impl PaneMenuState { pub(super) fn item_menu(menu: PopupMenu, state: &PaneMenuState) -> PopupMenu { let remote = state.remote; let none = state.targets.is_empty() || !state.can_modify; - let menu = if state.opens_directory && state.targets.len() == 1 { - menu.menu_with_icon( - "打开", - Icon::new(IconName::FolderOpen), - state.action(ExplorerCommand::Open { remote }), - ) - .separator() - } else if let Some(path) = state.edits_file.clone() { + // A directory has no 打开: a double-click goes in. + let menu = if let Some(path) = state.edits_file.clone() { // A picture is only previewed: as text it would be refused. An SVG // is text, and is both edited and previewed. let kind = state.preview_kind; @@ -93,6 +85,12 @@ pub(super) fn item_menu(menu: PopupMenu, state: &PaneMenuState) -> PopupMenu { state.action(ExplorerCommand::Transfer { remote }), state.targets.is_empty() || !state.can_transfer, ) + .menu_with_icon_and_disabled( + "复制路径", + Icon::new(IconName::Copy), + state.action(ExplorerCommand::CopySelectedPaths { remote }), + state.targets.is_empty(), + ) .separator() .menu_with_icon_and_disabled( "删除", diff --git a/tests/ui/sftp.rs b/tests/ui/sftp.rs index ac97cbc..a5cff03 100644 --- a/tests/ui/sftp.rs +++ b/tests/ui/sftp.rs @@ -573,6 +573,34 @@ async fn sftp_path_label_opens_ancestors_and_the_open_directory_dialog(cx: &mut let explorer = workspace.read(cx).explorer(ExplorerId(SFTP_TAB)).unwrap(); assert!(explorer.read(cx).local().read(cx).is_current()); }); + + // 复制路径 from a row's menu: the selected rows' full paths, a line each. + cx.update_window(handle.into(), |_, window, cx| { + window.render_frame(cx); + window + .within(("local-pane", SFTP_TAB)) + .click("name:文件 甲.txt", cx); + modified_click( + window, + ("local-pane", SFTP_TAB), + "name:目录", + gpui_kit::Modifiers::secondary_key(), + cx, + ); + window.dispatch_action( + Box::new(ExplorerAction::new( + ExplorerId(SFTP_TAB), + ExplorerCommand::CopySelectedPaths { remote: false }, + )), + cx, + ); + }) + .unwrap(); + cx.run_until_parked(); + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("/local/tester/目录\n/local/tester/文件 甲.txt".into()) + ); } #[gpui_kit::test]