explorer/、sftp/、editor/ 的文案移进 locales/explorer.yml、sftp.yml、editor.yml,中文逐字不变。 文件列表的列名、目录列表的分组跟着界面语言重设;列表占位改存枚举,拖动预览存方向, 「位置」改存 Place,显示时再翻译。动词拼句的地方(上传 / 下载、编辑器 / 预览、服务器上的 / 本机上的)拆成每种情况一个整句;废纸篓在 Windows 上写 Recycle Bin。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
725 lines
26 KiB
Rust
725 lines
26 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use gpui_kit::component::{
|
|
ActiveTheme as _, Disableable as _, Icon, Sizable as _, WindowExt as _,
|
|
button::{Button, ButtonVariants as _},
|
|
dialog::{DialogAction, DialogFooter},
|
|
dock::{BasePanel, Panel, PanelEvent, TabGroup},
|
|
h_flex,
|
|
input::{Editor, EditorState, InputEvent, Position, Rope, RopeExt as _},
|
|
menu::PopupMenu,
|
|
notification::Notification,
|
|
v_flex,
|
|
};
|
|
use gpui_kit::prelude::FluentBuilder as _;
|
|
use gpui_kit::*;
|
|
|
|
use super::{EditorId, EditorKey, cursor_label, format_label, indentation, language_for};
|
|
use crate::app::{
|
|
CatalogIcon, CenterTab, CloseEditor, EDITOR_CONTEXT, EditorAction, EditorCommand,
|
|
EditorShortcut,
|
|
};
|
|
use crate::explorer::{ExplorerId, ExplorerPanel, FileLocation};
|
|
use crate::host::{HostId, HostStore};
|
|
use crate::i18n::t;
|
|
use crate::sftp::{
|
|
FileStamp, ReadFailure, SaveFailure, SharedLocalDirectoryProvider, TextFile, TextFormat,
|
|
read_local_text, write_local_text,
|
|
};
|
|
use crate::shared::{ClosableTabTitle, HostMark, close_tab_items, confirm_danger, soft_tag};
|
|
|
|
/// Where an editor reads and writes its file. A remote file goes through
|
|
/// the SFTP tab that opened it, over that tab's connection; a local one
|
|
/// straight to the disk.
|
|
pub enum EditorSource {
|
|
Remote {
|
|
explorer: WeakEntity<ExplorerPanel>,
|
|
explorer_id: ExplorerId,
|
|
host: HostId,
|
|
},
|
|
Local {
|
|
provider: SharedLocalDirectoryProvider,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum EditorPanelEvent {
|
|
Activated(EditorId),
|
|
Closed(EditorId),
|
|
/// What the window's status line shows for this tab changed: the
|
|
/// cursor, or whether there are changes.
|
|
Changed(EditorId),
|
|
/// The file was written; panes showing its folder read it again.
|
|
Saved(EditorId),
|
|
/// A save asked to close the tab after it is done, and it is.
|
|
CloseRequested(EditorId),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum Phase {
|
|
Idle,
|
|
Saving,
|
|
Reloading,
|
|
}
|
|
|
|
/// A center tab that edits one text file.
|
|
pub struct EditorPanel {
|
|
id: EditorId,
|
|
source: EditorSource,
|
|
location: FileLocation,
|
|
store: Entity<HostStore>,
|
|
state: Entity<EditorState>,
|
|
/// The text as last read or saved, to tell whether there are changes.
|
|
saved: Rope,
|
|
format: TextFormat,
|
|
/// What the file looked like when read or saved; `None` once a save
|
|
/// broke off halfway, after which the file is no longer what was read.
|
|
stamp: Option<FileStamp>,
|
|
dirty: bool,
|
|
phase: Phase,
|
|
/// Close the tab once the save under way succeeds (保存 when closing).
|
|
close_after_save: bool,
|
|
cursor: Position,
|
|
/// The workspace's focus handle, which commands from dialogs and
|
|
/// buttons are dispatched on.
|
|
dispatch: FocusHandle,
|
|
tab_group: Option<WeakEntity<TabGroup>>,
|
|
_task: Option<Task<()>>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl EditorPanel {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
id: EditorId,
|
|
source: EditorSource,
|
|
location: FileLocation,
|
|
file: TextFile,
|
|
store: Entity<HostStore>,
|
|
dispatch: FocusHandle,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let language = language_for(&location.name());
|
|
let (text, format, stamp) = file.into_parts();
|
|
let tab = indentation(language, &text);
|
|
let state = cx.new(|cx| {
|
|
EditorState::new(window, cx)
|
|
.language(language)
|
|
.tab_size(tab)
|
|
.default_value(text)
|
|
});
|
|
let saved = state.read(cx).text().clone();
|
|
let subscriptions = vec![
|
|
cx.subscribe(&state, |this, _, event: &InputEvent, cx| {
|
|
if matches!(event, InputEvent::Change) {
|
|
this.update_dirty(cx);
|
|
}
|
|
}),
|
|
// The cursor moves without a change of text.
|
|
cx.observe(&state, |this, state, cx| {
|
|
let cursor = state.read(cx).cursor_position();
|
|
if cursor != this.cursor {
|
|
this.cursor = cursor;
|
|
cx.emit(EditorPanelEvent::Changed(this.id));
|
|
}
|
|
}),
|
|
cx.observe(&store, |_, _, cx| cx.notify()),
|
|
];
|
|
Self {
|
|
id,
|
|
source,
|
|
location,
|
|
store,
|
|
state,
|
|
saved,
|
|
format,
|
|
stamp: Some(stamp),
|
|
dirty: false,
|
|
phase: Phase::Idle,
|
|
close_after_save: false,
|
|
cursor: Position::default(),
|
|
dispatch,
|
|
tab_group: None,
|
|
_task: None,
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> EditorId {
|
|
self.id
|
|
}
|
|
/// The file this tab edits, to find it when the file is opened again.
|
|
pub fn key(&self) -> EditorKey {
|
|
match (&self.source, &self.location) {
|
|
(EditorSource::Remote { host, .. }, FileLocation::Remote(path)) => {
|
|
EditorKey::Remote(*host, path.clone())
|
|
}
|
|
_ => EditorKey::Local(PathBuf::from(self.location.path())),
|
|
}
|
|
}
|
|
pub fn location(&self) -> &FileLocation {
|
|
&self.location
|
|
}
|
|
pub fn name(&self) -> String {
|
|
self.location.name()
|
|
}
|
|
/// The SFTP tab a remote file is read and written through.
|
|
pub fn explorer_id(&self) -> Option<ExplorerId> {
|
|
match &self.source {
|
|
EditorSource::Remote { explorer_id, .. } => Some(*explorer_id),
|
|
EditorSource::Local { .. } => None,
|
|
}
|
|
}
|
|
pub fn host_id(&self) -> Option<HostId> {
|
|
match &self.source {
|
|
EditorSource::Remote { host, .. } => Some(*host),
|
|
EditorSource::Local { .. } => None,
|
|
}
|
|
}
|
|
/// There are changes not saved yet.
|
|
pub fn is_dirty(&self) -> bool {
|
|
self.dirty
|
|
}
|
|
pub fn state(&self) -> &Entity<EditorState> {
|
|
&self.state
|
|
}
|
|
pub fn tab_group(&self) -> Option<WeakEntity<TabGroup>> {
|
|
self.tab_group.clone()
|
|
}
|
|
pub fn contains_focus(&self, window: &Window, cx: &App) -> bool {
|
|
self.state
|
|
.read(cx)
|
|
.focus_handle(cx)
|
|
.contains_focused(window, cx)
|
|
}
|
|
/// 「行 12,列 5」 for the window's status line.
|
|
pub fn cursor_label(&self) -> String {
|
|
cursor_label(self.cursor.line, self.cursor.character)
|
|
}
|
|
/// 「UTF-8 · LF」 for the window's status line.
|
|
pub fn format_label(&self) -> String {
|
|
format_label(self.format)
|
|
}
|
|
|
|
pub fn execute(&mut self, command: EditorCommand, window: &mut Window, cx: &mut Context<Self>) {
|
|
match command {
|
|
EditorCommand::Save => self.save(false, window, cx),
|
|
EditorCommand::Overwrite { close } => {
|
|
self.close_after_save = close;
|
|
self.save(true, window, cx);
|
|
}
|
|
EditorCommand::SaveAndClose => {
|
|
if self.dirty {
|
|
self.close_after_save = true;
|
|
self.save(false, window, cx);
|
|
} else {
|
|
cx.emit(EditorPanelEvent::CloseRequested(self.id));
|
|
}
|
|
}
|
|
EditorCommand::Reload => {
|
|
if self.dirty {
|
|
let action = EditorAction::new(self.id, EditorCommand::ReloadConfirmed);
|
|
let dispatch = self.dispatch.clone();
|
|
confirm_danger(
|
|
t!("editor.reload.title", name = self.name()),
|
|
Some(t!("editor.reload.description")),
|
|
t!("editor.command.reload"),
|
|
std::rc::Rc::new(move |window, cx| send(&dispatch, &action, window, cx)),
|
|
window,
|
|
cx,
|
|
);
|
|
} else {
|
|
self.reload(window, cx);
|
|
}
|
|
}
|
|
EditorCommand::ReloadConfirmed => self.reload(window, cx),
|
|
// The workspace closes the tab.
|
|
EditorCommand::CloseConfirmed => {}
|
|
EditorCommand::CopyPath => {
|
|
cx.write_to_clipboard(ClipboardItem::new_string(self.location.path()))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_dirty(&mut self, cx: &mut Context<Self>) {
|
|
let dirty = {
|
|
let text = self.state.read(cx).text();
|
|
text.len() != self.saved.len() || *text != self.saved
|
|
};
|
|
if dirty != self.dirty {
|
|
self.dirty = dirty;
|
|
cx.emit(EditorPanelEvent::Changed(self.id));
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
/// Write the text back; `overwrite` skips the check that nobody changed
|
|
/// the file since.
|
|
fn save(&mut self, overwrite: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.phase != Phase::Idle || !self.dirty && !overwrite {
|
|
if self.close_after_save && !self.dirty {
|
|
self.close_after_save = false;
|
|
cx.emit(EditorPanelEvent::CloseRequested(self.id));
|
|
}
|
|
return;
|
|
}
|
|
// Typing goes on while the file is written; what is saved is this.
|
|
let snapshot = self.state.read(cx).text().clone();
|
|
let bytes = self.format.encode(&snapshot.to_string());
|
|
let expected = if overwrite { None } else { self.stamp };
|
|
let write = match &self.source {
|
|
EditorSource::Remote { explorer, .. } => {
|
|
let FileLocation::Remote(path) = &self.location else {
|
|
return;
|
|
};
|
|
let Some(explorer) = explorer.upgrade() else {
|
|
self.close_after_save = false;
|
|
return;
|
|
};
|
|
let path = path.clone();
|
|
// `None`: not connected, and the SFTP tab said so.
|
|
match explorer.update(cx, |explorer, cx| {
|
|
explorer.write_remote_file(path, bytes, expected, window, cx)
|
|
}) {
|
|
Some(write) => write,
|
|
None => {
|
|
self.close_after_save = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
EditorSource::Local { provider } => {
|
|
let (provider, path) = (provider.clone(), PathBuf::from(self.location.path()));
|
|
cx.background_spawn(async move {
|
|
write_local_text(provider.as_ref(), &path, &bytes, expected)
|
|
})
|
|
}
|
|
};
|
|
self.phase = Phase::Saving;
|
|
cx.notify();
|
|
self._task = Some(cx.spawn_in(window, async move |this, cx| {
|
|
let result = write.await;
|
|
let _ = this.update_in(cx, |this, window, cx| {
|
|
this.finish_save(snapshot, result, window, cx)
|
|
});
|
|
}));
|
|
}
|
|
|
|
fn finish_save(
|
|
&mut self,
|
|
snapshot: Rope,
|
|
result: Result<FileStamp, SaveFailure>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.phase = Phase::Idle;
|
|
let close = std::mem::take(&mut self.close_after_save);
|
|
let name = self.name();
|
|
match result {
|
|
Ok(stamp) => {
|
|
self.stamp = Some(stamp);
|
|
self.saved = snapshot;
|
|
self.update_dirty(cx);
|
|
cx.emit(EditorPanelEvent::Saved(self.id));
|
|
if close && !self.dirty {
|
|
cx.emit(EditorPanelEvent::CloseRequested(self.id));
|
|
}
|
|
}
|
|
Err(SaveFailure::Changed) => {
|
|
let description = if self.location.is_remote() {
|
|
t!("editor.changed.remote")
|
|
} else {
|
|
t!("editor.changed.local")
|
|
};
|
|
let action = EditorAction::new(self.id, EditorCommand::Overwrite { close });
|
|
let dispatch = self.dispatch.clone();
|
|
confirm_danger(
|
|
t!("editor.changed.title", name = name),
|
|
Some(description),
|
|
t!("editor.changed.overwrite"),
|
|
std::rc::Rc::new(move |window, cx| send(&dispatch, &action, window, cx)),
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
Err(SaveFailure::Interrupted(message)) => {
|
|
// What is on disk is neither what was read nor what was meant.
|
|
self.stamp = None;
|
|
window.push_notification(
|
|
Notification::error(t!("editor.save.interrupted", message = message))
|
|
.title(t!("editor.save.interrupted_title", name = name)),
|
|
cx,
|
|
);
|
|
}
|
|
Err(SaveFailure::Failed(message)) => window.push_notification(
|
|
Notification::error(message).title(t!("editor.save.failed", name = name)),
|
|
cx,
|
|
),
|
|
}
|
|
cx.emit(EditorPanelEvent::Changed(self.id));
|
|
cx.notify();
|
|
}
|
|
|
|
/// Read the file again, changes or not.
|
|
fn reload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.phase != Phase::Idle {
|
|
return;
|
|
}
|
|
let read = match &self.source {
|
|
EditorSource::Remote { explorer, .. } => {
|
|
let Some(explorer) = explorer.upgrade() else {
|
|
return;
|
|
};
|
|
let location = self.location.clone();
|
|
match explorer.update(cx, |explorer, cx| explorer.read_file(&location, window, cx))
|
|
{
|
|
Some(read) => read,
|
|
None => return,
|
|
}
|
|
}
|
|
EditorSource::Local { provider } => {
|
|
let (provider, path) = (provider.clone(), PathBuf::from(self.location.path()));
|
|
cx.background_spawn(async move { read_local_text(provider.as_ref(), &path) })
|
|
}
|
|
};
|
|
self.phase = Phase::Reloading;
|
|
cx.notify();
|
|
self._task = Some(cx.spawn_in(window, async move |this, cx| {
|
|
let result = read.await;
|
|
let _ = this.update_in(cx, |this, window, cx| {
|
|
this.finish_reload(result, window, cx)
|
|
});
|
|
}));
|
|
}
|
|
|
|
fn finish_reload(
|
|
&mut self,
|
|
result: Result<TextFile, ReadFailure>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.phase = Phase::Idle;
|
|
match result {
|
|
Ok(file) => {
|
|
let (text, format, stamp) = file.into_parts();
|
|
let line = self.cursor.line;
|
|
self.state.update(cx, |state, cx| {
|
|
state.set_value(text, window, cx);
|
|
// Back to the line the cursor was on, as near as the
|
|
// new text allows.
|
|
let last = state.text().lines_len().saturating_sub(1) as u32;
|
|
state.set_cursor_position(Position::new(line.min(last), 0), window, cx);
|
|
});
|
|
self.saved = self.state.read(cx).text().clone();
|
|
self.format = format;
|
|
self.stamp = Some(stamp);
|
|
self.update_dirty(cx);
|
|
}
|
|
Err(failure) => window.push_notification(
|
|
Notification::error(failure.to_string())
|
|
.title(t!("editor.reload.failed", name = self.name())),
|
|
cx,
|
|
),
|
|
}
|
|
cx.emit(EditorPanelEvent::Changed(self.id));
|
|
cx.notify();
|
|
}
|
|
|
|
/// Ask what to do with the changes before the tab closes: save them,
|
|
/// throw them away, or keep the tab.
|
|
pub fn confirm_close(&self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let name = self.name();
|
|
let dispatch = self.dispatch.clone();
|
|
let save = EditorAction::new(self.id, EditorCommand::SaveAndClose);
|
|
let discard = EditorAction::new(self.id, EditorCommand::CloseConfirmed);
|
|
let focus = self.state.read(cx).focus_handle(cx);
|
|
window.open_dialog(cx, move |dialog, _, _| {
|
|
dialog
|
|
.title(t!("editor.close.title", name = name))
|
|
.overlay_closable(false)
|
|
.child(div().text_sm().child(t!("editor.close.description")))
|
|
.footer(
|
|
DialogFooter::new()
|
|
.child(
|
|
Button::new("editor-close-cancel")
|
|
.label(t!("common.cancel"))
|
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
|
)
|
|
.child(
|
|
Button::new("editor-close-discard")
|
|
.danger()
|
|
.label(t!("editor.close.discard"))
|
|
.on_click({
|
|
let (dispatch, discard) = (dispatch.clone(), discard.clone());
|
|
move |_, window, cx| {
|
|
window.close_dialog(cx);
|
|
send(&dispatch, &discard, window, cx);
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
DialogAction::new().child(
|
|
Button::new("editor-close-save")
|
|
.primary()
|
|
.label(t!("common.save")),
|
|
),
|
|
),
|
|
)
|
|
.on_ok({
|
|
let (dispatch, save) = (dispatch.clone(), save.clone());
|
|
move |_, window, cx| {
|
|
send(&dispatch, &save, window, cx);
|
|
true
|
|
}
|
|
})
|
|
.on_close({
|
|
let focus = focus.clone();
|
|
move |_, window, cx| window.focus(&focus, cx)
|
|
})
|
|
});
|
|
}
|
|
|
|
fn display_path(&self, cx: &App) -> String {
|
|
match &self.source {
|
|
EditorSource::Remote { host, .. } => {
|
|
let host = self
|
|
.store
|
|
.read(cx)
|
|
.host(*host)
|
|
.map(|host| host.name.to_string())
|
|
.unwrap_or_default();
|
|
format!("{host}:{}", self.location.path())
|
|
}
|
|
EditorSource::Local { .. } => self.location.path(),
|
|
}
|
|
}
|
|
|
|
fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let id = self.id;
|
|
let path: SharedString = self.display_path(cx).into();
|
|
let leading = match &self.source {
|
|
EditorSource::Remote { host, .. } => {
|
|
let host = self.store.read(cx).host(*host);
|
|
HostMark::new(
|
|
("editor-host", id.0),
|
|
host.map(|host| host.name.clone()).unwrap_or_default(),
|
|
host.and_then(|host| host.os),
|
|
)
|
|
.small()
|
|
.into_any_element()
|
|
}
|
|
EditorSource::Local { .. } => Icon::new(CatalogIcon::Laptop)
|
|
.small()
|
|
.text_color(cx.theme().muted_foreground)
|
|
.into_any_element(),
|
|
};
|
|
let state = match self.phase {
|
|
Phase::Saving => Some((t!("editor.state.saving"), cx.theme().muted_foreground)),
|
|
Phase::Reloading => Some((t!("editor.state.reloading"), cx.theme().muted_foreground)),
|
|
Phase::Idle if self.dirty => Some((t!("editor.state.modified"), cx.theme().warning)),
|
|
Phase::Idle => None,
|
|
};
|
|
let command = |command: EditorCommand| {
|
|
let (dispatch, action) = (self.dispatch.clone(), EditorAction::new(id, command));
|
|
move |_: &ClickEvent, window: &mut Window, cx: &mut App| {
|
|
send(&dispatch, &action, window, cx)
|
|
}
|
|
};
|
|
h_flex()
|
|
.id(("editor-toolbar", id.0))
|
|
.flex_shrink_0()
|
|
.gap_2()
|
|
.px_2()
|
|
.py_1()
|
|
.border_b_1()
|
|
.border_color(cx.theme().border)
|
|
.child(leading)
|
|
.child(
|
|
div()
|
|
.id(("editor-path", id.0))
|
|
.test_support()
|
|
.aria_label(path.clone())
|
|
.flex_1()
|
|
.min_w_0()
|
|
.overflow_hidden()
|
|
.whitespace_nowrap()
|
|
.text_ellipsis_middle()
|
|
.text_sm()
|
|
.font_family(cx.theme().mono_font_family.clone())
|
|
.text_color(cx.theme().muted_foreground)
|
|
.tooltip({
|
|
let path = path.clone();
|
|
move |window, cx| {
|
|
gpui_kit::component::tooltip::Tooltip::new(path.clone())
|
|
.build(window, cx)
|
|
}
|
|
})
|
|
.child(path),
|
|
)
|
|
.when_some(state, |row, (text, color)| {
|
|
row.child(
|
|
div()
|
|
.id(("editor-state", id.0))
|
|
.test_support()
|
|
.aria_label(text.clone())
|
|
.flex_shrink_0()
|
|
.child(soft_tag(text, color)),
|
|
)
|
|
})
|
|
.child(
|
|
Button::new(("editor-reload", id.0))
|
|
.ghost()
|
|
.small()
|
|
.icon(Icon::new(CatalogIcon::RefreshCw))
|
|
.label(t!("editor.command.reload"))
|
|
.disabled(self.phase != Phase::Idle)
|
|
.on_click(command(EditorCommand::Reload)),
|
|
)
|
|
.child(
|
|
Button::new(("editor-save", id.0))
|
|
.small()
|
|
.icon(Icon::new(CatalogIcon::Save))
|
|
.label(t!("common.save"))
|
|
.disabled(!self.dirty || self.phase != Phase::Idle)
|
|
.tooltip_with_action(
|
|
t!("common.save"),
|
|
&EditorShortcut(EditorCommand::Save),
|
|
Some(EDITOR_CONTEXT),
|
|
)
|
|
.on_click(command(EditorCommand::Save)),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Dispatch an editor command on the workspace, after the current update:
|
|
/// from a dialog or a button, where nothing in the editor may hold focus.
|
|
fn send(dispatch: &FocusHandle, action: &EditorAction, window: &mut Window, cx: &mut App) {
|
|
let (focus, action) = (dispatch.clone(), action.clone());
|
|
window.defer(cx, move |window, cx| {
|
|
focus.dispatch_action(&action, window, cx)
|
|
});
|
|
}
|
|
|
|
impl EventEmitter<PanelEvent> for EditorPanel {}
|
|
impl EventEmitter<EditorPanelEvent> for EditorPanel {}
|
|
|
|
impl Focusable for EditorPanel {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
self.state.read(cx).focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl BasePanel for EditorPanel {
|
|
fn panel_name(&self) -> &'static str {
|
|
"EditorPanel"
|
|
}
|
|
|
|
/// Closing goes through `CloseEditor`, which asks about changes; see
|
|
/// `ClosableTabTitle`.
|
|
fn closable(&self, _: &App) -> bool {
|
|
false
|
|
}
|
|
|
|
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
if active {
|
|
let focus = self.state.read(cx).focus_handle(cx);
|
|
window.focus(&focus, cx);
|
|
cx.emit(EditorPanelEvent::Activated(self.id));
|
|
}
|
|
}
|
|
|
|
fn on_added_to(&mut self, group: WeakEntity<TabGroup>, _: &mut Window, _: &mut Context<Self>) {
|
|
self.tab_group = Some(group);
|
|
}
|
|
|
|
fn on_removed(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
self.tab_group = None;
|
|
self._task = None;
|
|
cx.emit(EditorPanelEvent::Closed(self.id));
|
|
}
|
|
}
|
|
|
|
impl Panel for EditorPanel {
|
|
fn title(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (id, group, panel) = (self.id, self.tab_group.clone(), cx.entity_id());
|
|
ClosableTabTitle::new(
|
|
("editor-tab", id.0),
|
|
Icon::new(CatalogIcon::FileText).small(),
|
|
self.name(),
|
|
)
|
|
.modified(self.dirty)
|
|
.closable(("close-editor", id.0), Box::new(CloseEditor(id)))
|
|
.context_menu(move |menu, _, cx| tab_menu(menu, id, group.clone(), panel, cx))
|
|
}
|
|
|
|
fn dropdown_menu(
|
|
&mut self,
|
|
menu: PopupMenu,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> PopupMenu {
|
|
tab_menu(menu, self.id, self.tab_group.clone(), cx.entity_id(), cx)
|
|
}
|
|
|
|
fn inner_padding(&self, _: &App) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The commands of an editor tab, shared by its context menu and the tab
|
|
/// bar's 「…」 menu.
|
|
fn tab_menu(
|
|
menu: PopupMenu,
|
|
id: EditorId,
|
|
group: Option<WeakEntity<TabGroup>>,
|
|
panel: EntityId,
|
|
cx: &App,
|
|
) -> PopupMenu {
|
|
let action = |command| Box::new(EditorAction::new(id, command));
|
|
let menu = menu
|
|
.menu_with_icon(
|
|
t!("common.save"),
|
|
Icon::new(CatalogIcon::Save),
|
|
action(EditorCommand::Save),
|
|
)
|
|
.menu_with_icon(
|
|
t!("editor.command.reload"),
|
|
Icon::new(CatalogIcon::RefreshCw),
|
|
action(EditorCommand::Reload),
|
|
)
|
|
.menu(
|
|
t!("editor.command.copy_path"),
|
|
action(EditorCommand::CopyPath),
|
|
)
|
|
.separator();
|
|
close_tab_items(menu, CenterTab::Editor(id), group, panel, cx)
|
|
}
|
|
|
|
impl Render for EditorPanel {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let id = self.id;
|
|
v_flex()
|
|
.id(("editor", id.0))
|
|
.test_support()
|
|
.key_context(EDITOR_CONTEXT)
|
|
.size_full()
|
|
.child(self.render_toolbar(cx))
|
|
.child(
|
|
div()
|
|
.id(("editor-text", id.0))
|
|
.test_support()
|
|
.flex_1()
|
|
.min_h_0()
|
|
.child(
|
|
Editor::new(&self.state)
|
|
.bordered(false)
|
|
.rounded_none()
|
|
.h_full()
|
|
.readonly(self.phase == Phase::Reloading)
|
|
.aria_label(t!("editor.text_label", name = self.name())),
|
|
),
|
|
)
|
|
}
|
|
}
|