SFTP 右键菜单:去掉目录的「打开」,加「复制路径」

文件名上的菜单不再有「打开」:目录双击就进入。「上传… / 下载…」下面
加「复制路径」,复制选中项目的完整路径,多个时每行一个,「..」是上级目录。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-06 09:29:05 +08:00
1 parent 40589c9931
commit f1ac434ebf
6 files changed
+62 -12

No files matched your search

+1 -1
View File
@@ -265,7 +265,7 @@ bash 在 shell 退出时才把这次执行的命令写进历史文件,所以
- 框选:在名称右边的空白、大小、类型、时间等列或列表下方按下拖动,画出选框,框住的行被选中;按住 ⌘ 或 Shift 开始拖动则加到已有选择上。拖到列表上下边缘时自动滚动。拖动文件名才是把文件拖到另一侧传输:按下未选中的文件名时只选中它(其他选择取消),拖走的就是它一个;按下已选中的文件名则拖走全部已选。拖动时只有到了另一侧列表上才显示「上传 / 下载 N 个项目」,在自己的列表或其他不能放下的地方,指针显示为禁止。
- 键盘:↑↓、Home、End、PageUp、PageDown 移动,加 Shift 扩展;空格或 Insert 切换当前行;⌘A 全选;Tab 切到另一侧。
- 右键未选中的名称格会先选中它;右键空白处取消选择并弹出空白处菜单。
- 文件名上的菜单:打开(目录)或编辑(文件;图片、SVG 和 Markdown 另有预览,SVG 以外的图片只有预览)、上传… / 下载…、删除、重命名…、属性…。
- 文件名上的菜单:编辑(文件;图片、SVG 和 Markdown 另有预览,SVG 以外的图片只有预览)、上传… / 下载…、复制路径(选中项目的完整路径,多个时每行一个)、删除、重命名…、属性…。目录双击进入,菜单里没有「打开」。
- 空白处的菜单:前往(上级 / 根 / 主目录 / 后退 / 前进)、刷新、添加路径到书签、新建。
### 快捷键
+5
View File
@@ -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,
+20
View File
@@ -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<String> = 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
-1
View File
@@ -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))
+8 -10
View File
@@ -14,8 +14,6 @@ pub(super) struct PaneMenuState {
pub explorer: ExplorerId,
/// The rows the item commands act on (the selection).
pub targets: Vec<String>,
/// 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<String>,
@@ -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(
"删除",
+28
View File
@@ -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]