Files
shell-rs/src/explorer/file_dialogs.rs
T
XuzandClaude Opus 5.5 a0ed1ac8bf SFTP 与编辑器的文案接入多语言
explorer/、sftp/、editor/ 的文案移进 locales/explorer.yml、sftp.yml、editor.yml,中文逐字不变。
文件列表的列名、目录列表的分组跟着界面语言重设;列表占位改存枚举,拖动预览存方向,
「位置」改存 Place,显示时再翻译。动词拼句的地方(上传 / 下载、编辑器 / 预览、服务器上的 /
本机上的)拆成每种情况一个整句;废纸篓在 Windows 上写 Recycle Bin。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:12:51 +08:00

258 lines
8.9 KiB
Rust

//! The delete confirmation and the name form shared by 重命名 and 新建.
use super::{ExplorerPanel, NewEntryKind, PaneOperation};
use crate::app::ExplorerCommand;
use crate::i18n::{t, tn};
use crate::shared::{commit_footer, dismiss_form_error, form_error_notification};
use gpui_kit::component::{
Sizable as _, WindowExt as _,
button::ButtonVariant,
dialog::DialogButtonProps,
form::{Field, Form},
input::{Input, InputState},
v_flex,
};
use gpui_kit::*;
/// Check a name typed for a new or renamed item. `existing` is the current
/// listing; the server or file system still has the final word.
pub fn validate_entry_name(
name: &str,
original: Option<&str>,
existing: &[String],
) -> Result<(), String> {
if name.trim().is_empty() {
return Err(t!("explorer.name.empty").into());
}
if matches!(name, "." | "..") {
return Err(t!("explorer.name.dots").into());
}
if name.contains(['/', '\0']) {
return Err(t!("explorer.name.slash").into());
}
if original == Some(name) {
return Err(t!("explorer.name.unchanged").into());
}
if existing.iter().any(|existing| existing == name) {
return Err(t!("explorer.name.exists", name = name).into());
}
Ok(())
}
/// What the name form commits.
#[derive(Clone)]
enum NameIntent {
Rename(String),
New(NewEntryKind),
}
struct NameForm {
input: Entity<InputState>,
label: SharedString,
}
impl Render for NameForm {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_flex().gap_2().child(
Form::new().child(
Field::new()
.label(self.label.clone())
.required(true)
.child(Input::new(&self.input).id("entry-name").small()),
),
)
}
}
impl ExplorerPanel {
/// 删除: confirm with the scope and consequence, then delete. Local
/// items go to the Trash; remote ones are gone for good.
pub(super) fn confirm_delete(
&mut self,
remote: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let names = self.pane(remote).read(cx).selected_names(cx);
if names.is_empty() || !self.can_modify(remote, cx) || window.has_active_dialog(cx) {
return;
}
let title = if let [name] = names.as_slice() {
t!("explorer.delete.title_one", name = name)
} else {
tn!("explorer.delete.title_several", names.len())
};
let description = if remote {
t!("explorer.delete.remote")
} else if cfg!(windows) {
t!("explorer.delete.recycle_bin")
} else {
t!("explorer.delete.trash")
};
let sender = self.sender();
let focus = window.focused(cx);
window.open_alert_dialog(cx, move |dialog, _, _| {
dialog
.title(title.clone())
.description(description.clone())
.button_props(
DialogButtonProps::default()
.ok_text(t!("common.delete"))
.ok_variant(ButtonVariant::Danger)
.cancel_text(t!("common.cancel")),
)
.show_cancel(true)
.on_cancel({
let focus = focus.clone();
move |_, window, cx| {
if let Some(focus) = &focus {
window.focus(focus, cx);
}
true
}
})
.on_ok({
let (sender, focus, names) = (sender.clone(), focus.clone(), names.clone());
move |_, window, cx| {
let operation = PaneOperation::Delete(names.clone());
sender.send(ExplorerCommand::Operate { remote, operation }, window, cx);
if let Some(focus) = &focus {
window.focus(focus, cx);
}
true
}
})
});
}
pub(super) fn open_rename(
&mut self,
remote: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let [name] = self.pane(remote).read(cx).selected_names(cx).as_slice() {
let name = name.clone();
self.open_name_dialog(remote, NameIntent::Rename(name), window, cx);
}
}
pub(super) fn open_new(
&mut self,
remote: bool,
kind: NewEntryKind,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_name_dialog(remote, NameIntent::New(kind), window, cx);
}
fn open_name_dialog(
&mut self,
remote: bool,
intent: NameIntent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.can_modify(remote, cx) || window.has_active_dialog(cx) {
return;
}
let existing: Vec<String> = self
.pane(remote)
.read(cx)
.listed_entries(cx)
.iter()
.filter(|entry| !entry.is_parent())
.map(|entry| entry.name.to_string())
.collect();
let (title, initial, commit, label) = match &intent {
NameIntent::Rename(name) => (
t!("explorer.rename.title", name = name),
name.clone(),
t!("explorer.rename.commit"),
t!("explorer.rename.label"),
),
NameIntent::New(kind) => (
kind.title(),
kind.default_name().to_string(),
t!("explorer.new.commit"),
t!("explorer.new.label"),
),
};
let input = cx.new(|cx| InputState::new(window, cx).default_value(initial));
let form = cx.new(|_| NameForm { input, label });
let sender = self.sender();
let focus = window.focused(cx);
window.open_dialog(cx, move |dialog, _, _| {
dialog
.title(title.clone())
// Closed by its buttons or Escape, not by a click beside it.
.overlay_closable(false)
.child(form.clone())
.footer(commit_footer("commit", commit.clone()))
.on_ok({
let (form, sender, intent, existing) = (
form.clone(),
sender.clone(),
intent.clone(),
existing.clone(),
);
move |_, window, cx| {
let name = form.read(cx).input.read(cx).value().to_string();
let original = match &intent {
NameIntent::Rename(from) => Some(from.as_str()),
NameIntent::New(_) => None,
};
if let Err(error) = validate_entry_name(&name, original, &existing) {
window.push_notification(form_error_notification(error), cx);
return false;
}
let operation = match &intent {
NameIntent::Rename(from) => PaneOperation::Rename {
from: from.clone(),
to: name,
},
NameIntent::New(kind) => PaneOperation::Create { kind: *kind, name },
};
sender.send(ExplorerCommand::Operate { remote, operation }, window, cx);
true
}
})
.on_close({
let focus = focus.clone();
move |_, window, cx| {
dismiss_form_error(window, cx);
if let Some(focus) = &focus {
window.focus(focus, cx);
}
}
})
});
}
}
#[cfg(test)]
mod tests {
use super::validate_entry_name;
#[test]
fn names_are_checked_against_the_listing() {
let existing = vec!["a.txt".to_string(), "目录".to_string()];
assert!(validate_entry_name("b.txt", None, &existing).is_ok());
assert_eq!(
validate_entry_name(" ", None, &existing).unwrap_err(),
"名称不能为空"
);
assert!(validate_entry_name("..", None, &existing).is_err());
assert!(validate_entry_name("a/b", None, &existing).is_err());
assert_eq!(
validate_entry_name("a.txt", Some("a.txt"), &existing).unwrap_err(),
"名称未改变"
);
assert_eq!(
validate_entry_name("目录", Some("a.txt"), &existing).unwrap_err(),
"已有名为「目录」的项目"
);
assert!(validate_entry_name("A.txt", Some("a.txt"), &existing).is_ok());
}
}