feat: add file editor vim mode (#1437)
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d9b9b56599
commit
f45fe05f33
@@ -333,7 +333,7 @@ chown -R 1000:1000 data/
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>Files, Diff & Terminal</strong></summary>
|
<summary><strong>Files, Diff & Terminal</strong></summary>
|
||||||
|
|
||||||
- Workspace file browser with inline editing, syntax highlighting, and markdown preview
|
- Workspace file browser with inline editing, syntax highlighting, Vim mode, and markdown preview
|
||||||
- Beautiful diff viewer with stacked/inline modes, lazy loading for large changesets
|
- Beautiful diff viewer with stacked/inline modes, lazy loading for large changesets
|
||||||
- Integrated terminal with per-directory sessions, tabbed interface, and stable heavy-output performance
|
- Integrated terminal with per-directory sessions, tabbed interface, and stable heavy-output performance
|
||||||
- Clickable file paths in messages - jump to exact line locations
|
- Clickable file paths in messages - jump to exact line locations
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.16.0",
|
"@opencode-ai/sdk": "^1.16.0",
|
||||||
"@pierre/diffs": "1.1.0-beta.13",
|
"@pierre/diffs": "1.1.0-beta.13",
|
||||||
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@simplewebauthn/browser": "13.3.0",
|
"@simplewebauthn/browser": "13.3.0",
|
||||||
"@tanstack/react-virtual": "^3.13.18",
|
"@tanstack/react-virtual": "^3.13.18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
@@ -1092,6 +1093,8 @@
|
|||||||
|
|
||||||
"@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="],
|
"@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="],
|
||||||
|
|
||||||
|
"@replit/codemirror-vim": ["@replit/codemirror-vim@6.3.0", "", { "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||||
|
|
||||||
"@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="],
|
"@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="],
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.16.0",
|
"@opencode-ai/sdk": "^1.16.0",
|
||||||
"@pierre/diffs": "1.1.0-beta.13",
|
"@pierre/diffs": "1.1.0-beta.13",
|
||||||
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@simplewebauthn/browser": "13.3.0",
|
"@simplewebauthn/browser": "13.3.0",
|
||||||
"@tanstack/react-virtual": "^3.13.18",
|
"@tanstack/react-virtual": "^3.13.18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const VisualSectionContent: React.FC = () => {
|
|||||||
...(!isVSCode ? ['weekStart' as const] : []),
|
...(!isVSCode ? ['weekStart' as const] : []),
|
||||||
'fontSize',
|
'fontSize',
|
||||||
'terminalFontSize',
|
'terminalFontSize',
|
||||||
|
'fileEditorKeymap',
|
||||||
'spacing',
|
'spacing',
|
||||||
'inputBarOffset',
|
'inputBarOffset',
|
||||||
...(!isVSCode ? ['terminalQuickKeys' as const] : []),
|
...(!isVSCode ? ['terminalQuickKeys' as const] : []),
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
|||||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
|
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
|
||||||
|
|
||||||
interface OpenChamberVisualSettingsProps {
|
interface OpenChamberVisualSettingsProps {
|
||||||
/** Which settings to show. If undefined, shows all. */
|
/** Which settings to show. If undefined, shows all. */
|
||||||
@@ -297,6 +297,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const setDiffViewMode = useUIStore(state => state.setDiffViewMode);
|
const setDiffViewMode = useUIStore(state => state.setDiffViewMode);
|
||||||
const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop);
|
const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop);
|
||||||
const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop);
|
const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop);
|
||||||
|
const fileEditorKeymap = useUIStore(state => state.fileEditorKeymap);
|
||||||
|
const setFileEditorKeymap = useUIStore(state => state.setFileEditorKeymap);
|
||||||
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
||||||
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
||||||
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
||||||
@@ -516,7 +518,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
? hasLocalizationSettings
|
? hasLocalizationSettings
|
||||||
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
|
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
|
||||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
||||||
const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile;
|
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || shouldShow('fileEditorKeymap');
|
||||||
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
||||||
|| shouldShow('userMessageRendering')
|
|| shouldShow('userMessageRendering')
|
||||||
|| shouldShow('chatRenderMode')
|
|| shouldShow('chatRenderMode')
|
||||||
@@ -1211,6 +1213,48 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<section className="px-2 pb-2 pt-0">
|
<section className="px-2 pb-2 pt-0">
|
||||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.navigation')}</h4>
|
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.navigation')}</h4>
|
||||||
|
{shouldShow('fileEditorKeymap') && (
|
||||||
|
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-start sm:gap-8">
|
||||||
|
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">
|
||||||
|
{t('settings.openchamber.visual.field.fileEditorKeymap')}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={t('settings.openchamber.visual.field.fileEditorKeymap')}
|
||||||
|
className="space-y-0"
|
||||||
|
>
|
||||||
|
{(['default', 'vim'] as const).map((keymap) => {
|
||||||
|
const selected = fileEditorKeymap === keymap;
|
||||||
|
const labelText = t(`settings.openchamber.visual.option.fileEditorKeymap.${keymap}`);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={keymap}
|
||||||
|
type="button"
|
||||||
|
className="flex cursor-pointer items-center gap-2 py-0.5 text-left"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={selected}
|
||||||
|
onClick={() => setFileEditorKeymap(keymap)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-[14px] w-[14px] min-h-[14px] min-w-[14px] shrink-0 self-center items-center justify-center rounded-full transition-[background-color,box-shadow] duration-200 ease-out',
|
||||||
|
selected
|
||||||
|
? 'bg-[color-mix(in_srgb,var(--primary-base)_80%,transparent)] shadow-none'
|
||||||
|
: 'bg-[var(--surface-muted)] shadow-[inset_0_0_0_1px_var(--interactive-border)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn('block h-[5px] w-[5px] rounded-full bg-white', !selected && 'opacity-0')} />
|
||||||
|
</span>
|
||||||
|
<span className={cn('typography-ui-label font-normal', selected ? 'text-foreground' : 'text-foreground/50')}>
|
||||||
|
{labelText}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{shouldShow('terminalQuickKeys') && !isMobile && (
|
{shouldShow('terminalQuickKeys') && !isMobile && (
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { forceParsing, indentUnit } from '@codemirror/language';
|
|||||||
import { search, searchKeymap, openSearchPanel, closeSearchPanel, searchPanelOpen, getSearchQuery } from '@codemirror/search';
|
import { search, searchKeymap, openSearchPanel, closeSearchPanel, searchPanelOpen, getSearchQuery } from '@codemirror/search';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { createVimModeExtensions } from '@/lib/codemirror/vimModeExtension';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/** Patches `title` attributes onto CodeMirror search-panel controls for icon-only tooltips. */
|
/** Patches `title` attributes onto CodeMirror search-panel controls for icon-only tooltips. */
|
||||||
@@ -141,6 +142,7 @@ type CodeMirrorEditorProps = {
|
|||||||
enableSearch?: boolean;
|
enableSearch?: boolean;
|
||||||
searchOpen?: boolean;
|
searchOpen?: boolean;
|
||||||
onSearchOpenChange?: (open: boolean) => void;
|
onSearchOpenChange?: (open: boolean) => void;
|
||||||
|
vimMode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lineNumbersCompartment = new Compartment();
|
const lineNumbersCompartment = new Compartment();
|
||||||
@@ -149,6 +151,7 @@ const externalExtensionsCompartment = new Compartment();
|
|||||||
const highlightLinesCompartment = new Compartment();
|
const highlightLinesCompartment = new Compartment();
|
||||||
const blockWidgetsCompartment = new Compartment();
|
const blockWidgetsCompartment = new Compartment();
|
||||||
const searchCompartment = new Compartment();
|
const searchCompartment = new Compartment();
|
||||||
|
const vimCompartment = new Compartment();
|
||||||
|
|
||||||
const toViewKeyBindings = (bindings: readonly unknown[]): readonly KeyBinding[] => {
|
const toViewKeyBindings = (bindings: readonly unknown[]): readonly KeyBinding[] => {
|
||||||
return bindings as readonly KeyBinding[];
|
return bindings as readonly KeyBinding[];
|
||||||
@@ -265,6 +268,7 @@ export function CodeMirrorEditor({
|
|||||||
enableSearch,
|
enableSearch,
|
||||||
searchOpen,
|
searchOpen,
|
||||||
onSearchOpenChange,
|
onSearchOpenChange,
|
||||||
|
vimMode,
|
||||||
}: CodeMirrorEditorProps) {
|
}: CodeMirrorEditorProps) {
|
||||||
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
const hostRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const viewRef = React.useRef<EditorView | null>(null);
|
const viewRef = React.useRef<EditorView | null>(null);
|
||||||
@@ -274,6 +278,7 @@ export function CodeMirrorEditor({
|
|||||||
const onViewDestroyRef = React.useRef(onViewDestroy);
|
const onViewDestroyRef = React.useRef(onViewDestroy);
|
||||||
const onSearchOpenChangeRef = React.useRef(onSearchOpenChange);
|
const onSearchOpenChangeRef = React.useRef(onSearchOpenChange);
|
||||||
const blockWidgetsRef = React.useRef(blockWidgets);
|
const blockWidgetsRef = React.useRef(blockWidgets);
|
||||||
|
const vimModeRef = React.useRef(vimMode);
|
||||||
|
|
||||||
// Scoped map for widget containers to avoid global collisions and memory leaks
|
// Scoped map for widget containers to avoid global collisions and memory leaks
|
||||||
const widgetContainersRef = React.useRef(new Map<string, HTMLElement>());
|
const widgetContainersRef = React.useRef(new Map<string, HTMLElement>());
|
||||||
@@ -367,6 +372,7 @@ export function CodeMirrorEditor({
|
|||||||
lineNumbersCompartment.of(lineNumbers(lineNumbersConfig)),
|
lineNumbersCompartment.of(lineNumbers(lineNumbersConfig)),
|
||||||
history(),
|
history(),
|
||||||
indentUnit.of(' '),
|
indentUnit.of(' '),
|
||||||
|
vimCompartment.of(createVimModeExtensions(vimMode)),
|
||||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
syncEditorCssVars(update.view);
|
syncEditorCssVars(update.view);
|
||||||
@@ -449,6 +455,23 @@ export function CodeMirrorEditor({
|
|||||||
});
|
});
|
||||||
}, [extensions, highlightLines, lineNumbersConfig, readOnly, blockWidgets, enableSearch, syncEditorCssVars, syncPortalWidgets]);
|
}, [extensions, highlightLines, lineNumbersConfig, readOnly, blockWidgets, enableSearch, syncEditorCssVars, syncPortalWidgets]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const view = viewRef.current;
|
||||||
|
if (!view) {
|
||||||
|
vimModeRef.current = vimMode;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vimModeRef.current === vimMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vimModeRef.current = vimMode;
|
||||||
|
view.dispatch({
|
||||||
|
effects: vimCompartment.reconfigure(createVimModeExtensions(vimMode)),
|
||||||
|
});
|
||||||
|
}, [vimMode]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if (!view || enableSearch === false) {
|
if (!view || enableSearch === false) {
|
||||||
|
|||||||
@@ -906,10 +906,11 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
const setMainTabGuard = useUIStore((state) => state.setMainTabGuard);
|
const setMainTabGuard = useUIStore((state) => state.setMainTabGuard);
|
||||||
const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation);
|
const pendingFileNavigation = useUIStore((state) => state.pendingFileNavigation);
|
||||||
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
|
const setPendingFileNavigation = useUIStore((state) => state.setPendingFileNavigation);
|
||||||
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
|
const pendingFileFocusPath = useUIStore((state) => state.pendingFileFocusPath);
|
||||||
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
|
const setPendingFileFocusPath = useUIStore((state) => state.setPendingFileFocusPath);
|
||||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||||
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
const fileEditorKeymap = useUIStore((state) => state.fileEditorKeymap);
|
||||||
|
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
||||||
|
|
||||||
// Global mouseup to end drag selection
|
// Global mouseup to end drag selection
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -3555,11 +3556,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
>
|
>
|
||||||
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor
|
||||||
value={draftContent}
|
value={draftContent}
|
||||||
onChange={setDraftContent}
|
onChange={setDraftContent}
|
||||||
readOnly={!canEdit}
|
readOnly={!canEdit}
|
||||||
extensions={editorExtensions}
|
vimMode={fileEditorKeymap === 'vim'}
|
||||||
className="h-full"
|
extensions={editorExtensions}
|
||||||
|
className="h-full"
|
||||||
blockWidgets={blockWidgets}
|
blockWidgets={blockWidgets}
|
||||||
onViewReady={(view) => {
|
onViewReady={(view) => {
|
||||||
editorViewRef.current = view;
|
editorViewRef.current = view;
|
||||||
@@ -3852,11 +3854,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
<div className={cn('relative h-full', shouldMaskEditorForPendingNavigation && 'overflow-hidden')}>
|
<div className={cn('relative h-full', shouldMaskEditorForPendingNavigation && 'overflow-hidden')}>
|
||||||
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
<div className={cn('h-full', shouldMaskEditorForPendingNavigation && 'invisible')}>
|
||||||
<CodeMirrorEditor
|
<CodeMirrorEditor
|
||||||
value={draftContent}
|
value={draftContent}
|
||||||
onChange={setDraftContent}
|
onChange={setDraftContent}
|
||||||
readOnly={!canEdit}
|
readOnly={!canEdit}
|
||||||
extensions={editorExtensions}
|
vimMode={fileEditorKeymap === 'vim'}
|
||||||
className="h-full"
|
extensions={editorExtensions}
|
||||||
|
className="h-full"
|
||||||
onViewReady={(view) => {
|
onViewReady={(view) => {
|
||||||
editorViewRef.current = view;
|
editorViewRef.current = view;
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -143,6 +143,20 @@ export function createFlexokiCodeMirrorTheme(theme: Theme): Extension {
|
|||||||
left: 'auto',
|
left: 'auto',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
},
|
},
|
||||||
|
'.cm-panels-bottom': {
|
||||||
|
position: 'static' as const,
|
||||||
|
width: '100%',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
},
|
||||||
|
'.cm-panels-bottom .cm-vim-panel': {
|
||||||
|
borderTop: `1px solid ${theme.colors.interactive.border}`,
|
||||||
|
backgroundColor: theme.colors.surface.elevated,
|
||||||
|
color: theme.colors.surface.mutedForeground,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: '12px',
|
||||||
|
minHeight: '22px',
|
||||||
|
padding: '2px 8px',
|
||||||
|
},
|
||||||
|
|
||||||
/* ── Floating search panel ── */
|
/* ── Floating search panel ── */
|
||||||
'.cm-panel.cm-search': {
|
'.cm-panel.cm-search': {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
import type { EditorView, Panel } from '@codemirror/view';
|
||||||
|
import { drawSelection, showPanel } from '@codemirror/view';
|
||||||
|
import { getCM, vim } from '@replit/codemirror-vim';
|
||||||
|
|
||||||
|
type VimStatusPlugin = {
|
||||||
|
updateStatus: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isVimStatusPlugin = (value: unknown): value is VimStatusPlugin => {
|
||||||
|
return (
|
||||||
|
value !== null
|
||||||
|
&& typeof value === 'object'
|
||||||
|
&& 'updateStatus' in value
|
||||||
|
&& typeof value.updateStatus === 'function'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bottomVimStatusPanel = (view: EditorView): Panel => {
|
||||||
|
const dom = document.createElement('div');
|
||||||
|
dom.className = 'cm-vim-panel';
|
||||||
|
|
||||||
|
const cm = getCM(view);
|
||||||
|
if (!cm) {
|
||||||
|
return { top: false, dom };
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.state.statusbar = dom;
|
||||||
|
const vimPlugin: unknown = cm.state.vimPlugin;
|
||||||
|
// The Vim package exposes only a top status panel; reuse its updater with a bottom panel.
|
||||||
|
if (isVimStatusPlugin(vimPlugin)) {
|
||||||
|
vimPlugin.updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: false,
|
||||||
|
dom,
|
||||||
|
destroy() {
|
||||||
|
if (cm.state.statusbar === dom) {
|
||||||
|
cm.state.statusbar = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createVimModeExtensions(enabled: boolean | undefined): Extension[] {
|
||||||
|
if (!enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [vim(), showPanel.of(bottomVimStatusPanel), drawSelection()];
|
||||||
|
}
|
||||||
@@ -1605,6 +1605,9 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal quick keys',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal quick keys',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Quick Keys',
|
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Quick Keys',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, Ctrl, Arrows in terminal view',
|
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, Ctrl, Arrows in terminal view',
|
||||||
|
'settings.openchamber.visual.field.fileEditorKeymap': 'File editor keymap',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Default',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
|
||||||
'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity default mode: {option}',
|
'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity default mode: {option}',
|
||||||
'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Show expanded bash tools',
|
'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Show expanded bash tools',
|
||||||
'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Show expanded edit tools',
|
'settings.openchamber.visual.field.showExpandedEditToolsAria': 'Show expanded edit tools',
|
||||||
|
|||||||
@@ -1572,6 +1572,9 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas del terminal",
|
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas del terminal",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas del terminal",
|
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas del terminal",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl y flechas en la vista del terminal",
|
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl y flechas en la vista del terminal",
|
||||||
|
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas del editor de archivos",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.default": "Predeterminado",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
|
||||||
"settings.openchamber.visual.field.activityDefaultModeAria": "Modo predeterminado de actividad: {option}",
|
"settings.openchamber.visual.field.activityDefaultModeAria": "Modo predeterminado de actividad: {option}",
|
||||||
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar herramientas de Bash expandidas",
|
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar herramientas de Bash expandidas",
|
||||||
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar herramientas de edición expandidas",
|
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar herramientas de edición expandidas",
|
||||||
|
|||||||
@@ -1572,6 +1572,9 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': '터미널 빠른 키',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': '터미널 빠른 키',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeys': '터미널 빠른 키',
|
'settings.openchamber.visual.field.terminalQuickKeys': '터미널 빠른 키',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, Ctrl, 화살표를 표시합니다',
|
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, Ctrl, 화살표를 표시합니다',
|
||||||
|
'settings.openchamber.visual.field.fileEditorKeymap': '파일 편집기 키맵',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.default': '기본값',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
|
||||||
'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity 기본 모드: {option}',
|
'settings.openchamber.visual.field.activityDefaultModeAria': 'Activity 기본 모드: {option}',
|
||||||
'settings.openchamber.visual.field.showExpandedBashToolsAria': '확장된 Bash 도구 표시',
|
'settings.openchamber.visual.field.showExpandedBashToolsAria': '확장된 Bash 도구 표시',
|
||||||
'settings.openchamber.visual.field.showExpandedEditToolsAria': '확장된 편집 도구 표시',
|
'settings.openchamber.visual.field.showExpandedEditToolsAria': '확장된 편집 도구 표시',
|
||||||
|
|||||||
@@ -949,6 +949,9 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.terminalQuickKeys': 'Szybkie klawisze terminala',
|
'settings.openchamber.visual.field.terminalQuickKeys': 'Szybkie klawisze terminala',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Szybkie klawisze terminala',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Szybkie klawisze terminala',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, Ctrl i strzałki w widoku terminala',
|
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, Ctrl i strzałki w widoku terminala',
|
||||||
|
'settings.openchamber.visual.field.fileEditorKeymap': 'Mapa klawiszy edytora plików',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Domyślna',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
|
||||||
'settings.openchamber.visual.field.themeImportInfoAria': 'Informacje o imporcie motywów',
|
'settings.openchamber.visual.field.themeImportInfoAria': 'Informacje o imporcie motywów',
|
||||||
'settings.openchamber.visual.field.themeImportInfoTooltip': 'Importuj własne motywy z ~/.config/openchamber/themes/',
|
'settings.openchamber.visual.field.themeImportInfoTooltip': 'Importuj własne motywy z ~/.config/openchamber/themes/',
|
||||||
'settings.openchamber.visual.field.timeFormat': 'Format czasu',
|
'settings.openchamber.visual.field.timeFormat': 'Format czasu',
|
||||||
|
|||||||
@@ -1572,6 +1572,9 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas do terminal",
|
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas do terminal",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas do terminal",
|
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas do terminal",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl e flechas na vista do terminal",
|
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl e flechas na vista do terminal",
|
||||||
|
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas do editor de arquivos",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.default": "Padrão",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
|
||||||
"settings.openchamber.visual.field.activityDefaultModeAria": "Modo padrão de atividade: {option}",
|
"settings.openchamber.visual.field.activityDefaultModeAria": "Modo padrão de atividade: {option}",
|
||||||
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar ferramentas de Bash expandidas",
|
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Mostrar ferramentas de Bash expandidas",
|
||||||
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar ferramentas de edição expandidas",
|
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Mostrar ferramentas de edição expandidas",
|
||||||
|
|||||||
@@ -1572,6 +1572,9 @@ export const settingsDict = {
|
|||||||
"settings.openchamber.visual.field.terminalQuickKeysAria": "Швидкі клавіші терміналу",
|
"settings.openchamber.visual.field.terminalQuickKeysAria": "Швидкі клавіші терміналу",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeys": "Швидкі клавіші терміналу",
|
"settings.openchamber.visual.field.terminalQuickKeys": "Швидкі клавіші терміналу",
|
||||||
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показати Esc, Ctrl, стрілки в поданні терміналу",
|
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показати Esc, Ctrl, стрілки в поданні терміналу",
|
||||||
|
"settings.openchamber.visual.field.fileEditorKeymap": "Розкладка клавіш редактора файлів",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.default": "Типова",
|
||||||
|
"settings.openchamber.visual.option.fileEditorKeymap.vim": "Vim",
|
||||||
"settings.openchamber.visual.field.activityDefaultModeAria": "Типовий режим активності: {option}",
|
"settings.openchamber.visual.field.activityDefaultModeAria": "Типовий режим активності: {option}",
|
||||||
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Показати розширені інструменти bash",
|
"settings.openchamber.visual.field.showExpandedBashToolsAria": "Показати розширені інструменти bash",
|
||||||
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Показати розширені інструменти редагування",
|
"settings.openchamber.visual.field.showExpandedEditToolsAria": "Показати розширені інструменти редагування",
|
||||||
|
|||||||
@@ -1572,6 +1572,9 @@ export const settingsDict = {
|
|||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': '终端快捷键',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': '终端快捷键',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeys': '终端快捷键',
|
'settings.openchamber.visual.field.terminalQuickKeys': '终端快捷键',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、Ctrl、方向键',
|
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、Ctrl、方向键',
|
||||||
|
'settings.openchamber.visual.field.fileEditorKeymap': '文件编辑器键位映射',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.default': '默认',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
|
||||||
'settings.openchamber.visual.field.activityDefaultModeAria': '活动默认模式:{option}',
|
'settings.openchamber.visual.field.activityDefaultModeAria': '活动默认模式:{option}',
|
||||||
'settings.openchamber.visual.field.showExpandedBashToolsAria': '默认展开 Bash 工具',
|
'settings.openchamber.visual.field.showExpandedBashToolsAria': '默认展开 Bash 工具',
|
||||||
'settings.openchamber.visual.field.showExpandedEditToolsAria': '默认展开编辑工具',
|
'settings.openchamber.visual.field.showExpandedEditToolsAria': '默认展开编辑工具',
|
||||||
|
|||||||
@@ -1493,6 +1493,9 @@
|
|||||||
'settings.openchamber.visual.field.terminalQuickKeysAria': '終端機快速鍵',
|
'settings.openchamber.visual.field.terminalQuickKeysAria': '終端機快速鍵',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeys': '終端機快速鍵',
|
'settings.openchamber.visual.field.terminalQuickKeys': '終端機快速鍵',
|
||||||
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、Ctrl、方向鍵',
|
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、Ctrl、方向鍵',
|
||||||
|
'settings.openchamber.visual.field.fileEditorKeymap': '檔案編輯器鍵位映射',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.default': '預設',
|
||||||
|
'settings.openchamber.visual.option.fileEditorKeymap.vim': 'Vim',
|
||||||
'settings.openchamber.visual.field.activityDefaultModeAria': '活動預設模式:{option}',
|
'settings.openchamber.visual.field.activityDefaultModeAria': '活動預設模式:{option}',
|
||||||
'settings.openchamber.visual.field.showExpandedBashToolsAria': '預設展開 Bash 工具',
|
'settings.openchamber.visual.field.showExpandedBashToolsAria': '預設展開 Bash 工具',
|
||||||
'settings.openchamber.visual.field.showExpandedEditToolsAria': '預設展開編輯工具',
|
'settings.openchamber.visual.field.showExpandedEditToolsAria': '預設展開編輯工具',
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export type ActivityRenderMode = 'collapsed' | 'summary';
|
|||||||
export type SessionRetentionAction = 'archive' | 'delete';
|
export type SessionRetentionAction = 'archive' | 'delete';
|
||||||
export type TimeFormatPreference = 'auto' | '12h' | '24h';
|
export type TimeFormatPreference = 'auto' | '12h' | '24h';
|
||||||
export type WeekStartPreference = 'auto' | 'sunday' | 'monday';
|
export type WeekStartPreference = 'auto' | 'sunday' | 'monday';
|
||||||
|
export type FileEditorKeymap = 'default' | 'vim';
|
||||||
|
|
||||||
|
function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap {
|
||||||
|
return value === 'vim' ? 'vim' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
type ContextPanelTab = {
|
type ContextPanelTab = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -622,6 +627,7 @@ interface UIStore {
|
|||||||
isExpandedInput: boolean;
|
isExpandedInput: boolean;
|
||||||
reportUsage: boolean;
|
reportUsage: boolean;
|
||||||
shortcutOverrides: Record<string, ShortcutCombo>;
|
shortcutOverrides: Record<string, ShortcutCombo>;
|
||||||
|
fileEditorKeymap: FileEditorKeymap;
|
||||||
|
|
||||||
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
@@ -771,6 +777,7 @@ interface UIStore {
|
|||||||
setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void;
|
setShortcutOverride: (actionId: string, combo: ShortcutCombo) => void;
|
||||||
clearShortcutOverride: (actionId: string) => void;
|
clearShortcutOverride: (actionId: string) => void;
|
||||||
resetAllShortcutOverrides: () => void;
|
resetAllShortcutOverrides: () => void;
|
||||||
|
setFileEditorKeymap: (value: FileEditorKeymap) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -899,6 +906,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
isExpandedInput: false,
|
isExpandedInput: false,
|
||||||
reportUsage: true,
|
reportUsage: true,
|
||||||
shortcutOverrides: {},
|
shortcutOverrides: {},
|
||||||
|
fileEditorKeymap: 'default',
|
||||||
|
|
||||||
setTheme: (theme) => {
|
setTheme: (theme) => {
|
||||||
set({ theme });
|
set({ theme });
|
||||||
@@ -2050,6 +2058,10 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ shortcutOverrides: {} });
|
set({ shortcutOverrides: {} });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFileEditorKeymap: (value) => {
|
||||||
|
set({ fileEditorKeymap: normalizeFileEditorKeymap(value) });
|
||||||
|
},
|
||||||
|
|
||||||
toggleExpandedInput: () => {
|
toggleExpandedInput: () => {
|
||||||
set((state) => ({ isExpandedInput: !state.isExpandedInput }));
|
set((state) => ({ isExpandedInput: !state.isExpandedInput }));
|
||||||
},
|
},
|
||||||
@@ -2149,6 +2161,8 @@ export const useUIStore = create<UIStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.fileEditorKeymap = normalizeFileEditorKeymap(state.fileEditorKeymap);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
@@ -2230,6 +2244,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed,
|
isMobileSessionStatusBarCollapsed: state.isMobileSessionStatusBarCollapsed,
|
||||||
mobileSessionFilterProjectId: state.mobileSessionFilterProjectId,
|
mobileSessionFilterProjectId: state.mobileSessionFilterProjectId,
|
||||||
shortcutOverrides: state.shortcutOverrides,
|
shortcutOverrides: state.shortcutOverrides,
|
||||||
|
fileEditorKeymap: state.fileEditorKeymap,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user