[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
132
components/editor/TextEditorTabView.tsx
Normal file
132
components/editor/TextEditorTabView.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* TextEditorTabView — thin wrapper that binds an editorTab entry to TextEditorPane.
|
||||
*
|
||||
* Each tab has its own instance (keyed by tabId), so Monaco is never torn down
|
||||
* on tab-switch — we just toggle CSS visibility via the `isVisible` prop.
|
||||
*/
|
||||
import type * as Monaco from 'monaco-editor';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { saveEditorTab } from '../../application/state/editorTabSave';
|
||||
import { editorTabStore, useEditorTab, type EditorTabId } from '../../application/state/editorTabStore';
|
||||
import { useIsEditorTabActive } from '../../application/state/activeTabStore';
|
||||
import { useTerminalHostTreeLayoutWidth } from '../../application/state/terminalHostTreeStore';
|
||||
import type { HotkeyScheme, KeyBinding } from '../../domain/models';
|
||||
import type { Host } from '../../types';
|
||||
import { toast } from '../ui/toast';
|
||||
import { TextEditorPane } from './TextEditorPane';
|
||||
|
||||
export interface TextEditorTabViewProps {
|
||||
tabId: EditorTabId;
|
||||
hotkeyScheme: HotkeyScheme;
|
||||
keyBindings: KeyBinding[];
|
||||
/** Host lookup for building the `host:remotePath` subtitle next to the filename. */
|
||||
hostById: Map<string, Host>;
|
||||
/** Routed into Monaco's Cmd/Ctrl+W command so closing the editor tab works
|
||||
* even when focus is inside the editor (Monaco otherwise swallows the event). */
|
||||
onRequestClose: (tabId: EditorTabId) => void;
|
||||
}
|
||||
|
||||
export function getTextEditorTabShellStyle(isVisible: boolean, hostTreeLayoutWidth: number): React.CSSProperties {
|
||||
return {
|
||||
...(isVisible ? null : { pointerEvents: 'none', visibility: 'hidden' }),
|
||||
zIndex: 20,
|
||||
left: hostTreeLayoutWidth,
|
||||
};
|
||||
}
|
||||
|
||||
export const TextEditorTabView: React.FC<TextEditorTabViewProps> = ({
|
||||
tabId,
|
||||
hotkeyScheme,
|
||||
keyBindings,
|
||||
hostById,
|
||||
onRequestClose,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const tab = useEditorTab(tabId);
|
||||
// Self-subscribe visibility so switching tabs only re-renders this editor
|
||||
// instance, not AppView/App.
|
||||
const isVisible = useIsEditorTabActive(tabId);
|
||||
const hostTreeLayoutWidth = useTerminalHostTreeLayoutWidth();
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(content: string, viewState: Monaco.editor.ICodeEditorViewState | null) => {
|
||||
editorTabStore.updateContent(tabId, content, viewState);
|
||||
},
|
||||
[tabId],
|
||||
);
|
||||
|
||||
const handleLanguageChange = useCallback(
|
||||
(lang: string) => {
|
||||
editorTabStore.setLanguage(tabId, lang);
|
||||
},
|
||||
[tabId],
|
||||
);
|
||||
|
||||
const handleToggleWordWrap = useCallback(() => {
|
||||
const current = editorTabStore.getTab(tabId);
|
||||
if (!current) return;
|
||||
editorTabStore.setWordWrap(tabId, !current.wordWrap);
|
||||
}, [tabId]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const ok = await saveEditorTab(tabId);
|
||||
if (ok) {
|
||||
toast.success(t('sftp.editor.saved'), 'SFTP');
|
||||
} else {
|
||||
const msg = editorTabStore.getTab(tabId)?.saveError ?? t('sftp.editor.saveFailed');
|
||||
toast.error(msg, 'SFTP');
|
||||
}
|
||||
}, [tabId, t]);
|
||||
|
||||
const handleRequestClose = useCallback(() => {
|
||||
onRequestClose(tabId);
|
||||
}, [onRequestClose, tabId]);
|
||||
|
||||
// Tab has been closed — render nothing (parent should remove this instance,
|
||||
// but guard here in case of a transient render before unmount).
|
||||
if (!tab) return null;
|
||||
|
||||
const isDirty = tab.content !== tab.baselineContent;
|
||||
// Subtitle shown next to the filename in the Pane header, e.g.
|
||||
// "Rainyun-114.66.26.174:/root/hello-server.go". Falls back to hostId when
|
||||
// we don't have a Host record (session may have been removed).
|
||||
const host = hostById.get(tab.hostId);
|
||||
const hostLabel = host?.label ?? tab.hostId;
|
||||
const subtitle = `${hostLabel}:${tab.remotePath}`;
|
||||
|
||||
return (
|
||||
// Sibling tab panels (VaultView, SftpView, TerminalLayerMount, LogView)
|
||||
// all fill their flex-1 parent via `absolute inset-0`. Match that here so
|
||||
// an inactive editor tab doesn't collapse to zero height in normal flow,
|
||||
// and an active one fills the viewport instead of stacking beneath others.
|
||||
// z-index high enough to stay above the terminal workspace while leaving
|
||||
// room for the shared host sidebar when it is open.
|
||||
<div
|
||||
style={getTextEditorTabShellStyle(isVisible, hostTreeLayoutWidth)}
|
||||
className="absolute top-0 right-0 bottom-0 min-h-0 flex flex-col bg-background"
|
||||
>
|
||||
<TextEditorPane
|
||||
chrome="tab"
|
||||
fileName={`${tab.fileName}${isDirty ? ' *' : ''}`}
|
||||
subtitle={subtitle}
|
||||
onRequestClose={handleRequestClose}
|
||||
content={tab.content}
|
||||
languageId={tab.languageId}
|
||||
wordWrap={tab.wordWrap}
|
||||
saving={tab.savingState === 'saving'}
|
||||
saveError={tab.saveError}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
keyBindings={keyBindings}
|
||||
onContentChange={handleContentChange}
|
||||
onLanguageChange={handleLanguageChange}
|
||||
onToggleWordWrap={handleToggleWordWrap}
|
||||
onSave={handleSave}
|
||||
initialViewState={tab.viewState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextEditorTabView;
|
||||
Reference in New Issue
Block a user