feat(editor): add vim mode
This commit is contained in:
1 parent
fe4908321e
commit
47238b689c
13 files changed
+136
-3
No files matched your search
@@ -213,6 +213,7 @@ const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
|
||||
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
|
||||
const editSqlSemanticDiagnosticsMode = ref<SqlSemanticDiagnosticsMode>(settingsStore.editorSettings.sqlSemanticDiagnosticsMode);
|
||||
const editSqlSemanticDiagnosticsEnabled = ref(settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution);
|
||||
@@ -512,6 +513,7 @@ watch(
|
||||
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
|
||||
editSqlSemanticDiagnosticsMode.value = settingsStore.editorSettings.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution;
|
||||
@@ -581,6 +583,7 @@ function hasChanges(): boolean {
|
||||
editShowExecutionTargetPicker.value !== settingsStore.editorSettings.showExecutionTargetPicker ||
|
||||
editAutoAliasTables.value !== settingsStore.editorSettings.autoAliasTables ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editVimModeEnabled.value !== settingsStore.editorSettings.vimModeEnabled ||
|
||||
editSqlSemanticDiagnosticsMode.value !== settingsStore.editorSettings.sqlSemanticDiagnosticsMode ||
|
||||
editSqlSemanticDiagnosticsEnabled.value !== settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled ||
|
||||
editConfirmDangerousSqlExecution.value !== settingsStore.editorSettings.confirmDangerousSqlExecution ||
|
||||
@@ -636,6 +639,7 @@ async function persistSettings() {
|
||||
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
vimModeEnabled: editVimModeEnabled.value,
|
||||
sqlSemanticDiagnosticsMode: editSqlSemanticDiagnosticsMode.value,
|
||||
confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value,
|
||||
confirmUnsavedSqlClose: editConfirmUnsavedSqlClose.value,
|
||||
@@ -700,6 +704,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
|
||||
@@ -765,6 +770,7 @@ function resetAllDefaults() {
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
|
||||
@@ -2164,6 +2170,14 @@ onUnmounted(cleanupPreviewEditor);
|
||||
<Switch id="editor-word-wrap" v-model="editWordWrap" class="mt-0.5" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-vim-mode">{{ t("settings.vimMode") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.vimModeDescription") }}</p>
|
||||
</div>
|
||||
<Switch id="editor-vim-mode" v-model="editVimModeEnabled" class="mt-0.5" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-auto-alias-tables">{{ t("settings.autoAliasTables") }}</Label>
|
||||
|
||||
@@ -192,10 +192,16 @@ let codeMirrorEditorSelection: typeof import("@codemirror/state").EditorSelectio
|
||||
let fontThemeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let codeMirrorTheme: import("@codemirror/state").Compartment | null = null;
|
||||
let wordWrapComp: import("@codemirror/state").Compartment | null = null;
|
||||
let vimModeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let readOnlyComp: import("@codemirror/state").Compartment | null = null;
|
||||
let runKeymapComp: import("@codemirror/state").Compartment | null = null;
|
||||
let completionComp: import("@codemirror/state").Compartment | null = null;
|
||||
let diagnosticComp: import("@codemirror/state").Compartment | null = null;
|
||||
let codeMirrorVim: typeof import("@replit/codemirror-vim").vim | null = null;
|
||||
let codeMirrorVimApi: typeof import("@replit/codemirror-vim").Vim | null = null;
|
||||
let codeMirrorGetVimCm: typeof import("@replit/codemirror-vim").getCM | null = null;
|
||||
let codeMirrorVimImportPromise: Promise<typeof import("@replit/codemirror-vim")> | null = null;
|
||||
let dbxVimCommandsConfigured = false;
|
||||
let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildSqlCompletionExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
@@ -236,6 +242,7 @@ let executableStatementRangeCache: ExecutableStatementRangeCache | null = null;
|
||||
let editorScrollbarPointerCleanup: (() => void) | null = null;
|
||||
const EDITOR_SCROLLBAR_POINTER_GUTTER_PX = 18;
|
||||
const tableNavigationHoverClass = "query-editor--table-navigation-hover";
|
||||
const DBX_VIM_SAVE_EVENT = "dbx-vim-save";
|
||||
|
||||
function editorThemeAppearance() {
|
||||
return isDark.value ? "dark" : "light";
|
||||
@@ -264,6 +271,7 @@ const queryEditorAppearanceSettings = computed(() => {
|
||||
customThemes: settings.customThemes,
|
||||
activeCustomThemeId: settings.activeCustomThemeId,
|
||||
wordWrap: settings.wordWrap,
|
||||
vimModeEnabled: settings.vimModeEnabled,
|
||||
shortcuts: settings.shortcuts,
|
||||
};
|
||||
});
|
||||
@@ -730,6 +738,53 @@ function wordWrapExtension() {
|
||||
return props.forceWordWrap || settingsStore.editorSettings.wordWrap ? editorViewModule.EditorView.lineWrapping : [];
|
||||
}
|
||||
|
||||
function vimModeExtension(enabled = settingsStore.editorSettings.vimModeEnabled) {
|
||||
if (!codeMirrorVim || !enabled) return [];
|
||||
const vimExtension = codeMirrorVim({ status: true });
|
||||
if (!codeMirrorPrec || !editorViewModule || !codeMirrorGetVimCm || !codeMirrorVimApi) return vimExtension;
|
||||
|
||||
// Beekeeper treats Vim as a first-class editor keymap. Keep it above DBX's
|
||||
// normal shortcuts so regular normal-mode keys are not stolen by other maps.
|
||||
return codeMirrorPrec.highest([
|
||||
editorViewModule.keymap.of([
|
||||
{
|
||||
key: "Ctrl-[",
|
||||
mac: "Ctrl-[",
|
||||
linux: "Ctrl-[",
|
||||
win: "Ctrl-[",
|
||||
run(currentView) {
|
||||
const cm = codeMirrorGetVimCm?.(currentView);
|
||||
if (cm?.state.vim?.insertMode) {
|
||||
codeMirrorVimApi?.exitInsertMode(cm as any, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]),
|
||||
vimExtension,
|
||||
]);
|
||||
}
|
||||
|
||||
function configureDbxVimCommands(vimApi: typeof import("@replit/codemirror-vim").Vim) {
|
||||
if (dbxVimCommandsConfigured) return;
|
||||
dbxVimCommandsConfigured = true;
|
||||
vimApi.defineEx("write", "w", (cm) => {
|
||||
cm.cm6?.contentDOM.dispatchEvent(new CustomEvent(DBX_VIM_SAVE_EVENT, { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCodeMirrorVim() {
|
||||
if (codeMirrorVim && codeMirrorVimApi && codeMirrorGetVimCm) return true;
|
||||
codeMirrorVimImportPromise ??= import("@replit/codemirror-vim");
|
||||
const { vim, Vim, getCM } = await codeMirrorVimImportPromise;
|
||||
codeMirrorVim = vim;
|
||||
codeMirrorVimApi = Vim;
|
||||
codeMirrorGetVimCm = getCM;
|
||||
configureDbxVimCommands(Vim);
|
||||
return true;
|
||||
}
|
||||
|
||||
function indentExtension() {
|
||||
if (!codeMirrorIndentUnit) return [];
|
||||
return codeMirrorIndentUnit.of(editorIndentUnit());
|
||||
@@ -2089,6 +2144,7 @@ onMounted(async () => {
|
||||
fontThemeComp = new Compartment();
|
||||
codeMirrorTheme = new Compartment();
|
||||
wordWrapComp = new Compartment();
|
||||
vimModeComp = new Compartment();
|
||||
readOnlyComp = new Compartment();
|
||||
runKeymapComp = new Compartment();
|
||||
completionComp = new Compartment();
|
||||
@@ -2201,6 +2257,9 @@ onMounted(async () => {
|
||||
|
||||
const initialSettings = settingsStore.editorSettings;
|
||||
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors());
|
||||
if (initialSettings.vimModeEnabled) {
|
||||
await ensureCodeMirrorVim();
|
||||
}
|
||||
|
||||
class RunStatementGutterMarker extends GutterMarker {
|
||||
toDOM() {
|
||||
@@ -2291,6 +2350,8 @@ onMounted(async () => {
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
crosshairCursor(),
|
||||
activeLineHighlighter,
|
||||
// Vim must be mounted before DBX/default keymaps so normal-mode keys are handled first.
|
||||
vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)),
|
||||
keymap.of([...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap]),
|
||||
langSql.sql({ dialect }),
|
||||
tooltips({ parent: document.body }),
|
||||
@@ -2377,6 +2438,10 @@ onMounted(async () => {
|
||||
window.setTimeout(flushImeComposition, 0);
|
||||
return false;
|
||||
},
|
||||
[DBX_VIM_SAVE_EVENT]() {
|
||||
emit("save");
|
||||
return true;
|
||||
},
|
||||
wheel(event) {
|
||||
if (!event.metaKey && !event.ctrlKey) return false;
|
||||
event.preventDefault();
|
||||
@@ -2622,7 +2687,7 @@ function getCurrentCustomThemeColors() {
|
||||
watch(
|
||||
[queryEditorAppearanceSettings, () => isDark.value],
|
||||
async ([ss]) => {
|
||||
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !runKeymapComp || !editorViewModule) {
|
||||
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
}
|
||||
if (!isGestureZooming.value && !zoomCommitScheduler.hasPendingCommit() && liveFontSize.value !== ss.fontSize) {
|
||||
@@ -2630,9 +2695,17 @@ watch(
|
||||
}
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
const themeColors = getCurrentCustomThemeColors();
|
||||
const themeExt = await loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors);
|
||||
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
|
||||
if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
}
|
||||
view.value.dispatch({
|
||||
effects: [codeMirrorTheme.reconfigure(themeExt), wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []), runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap))],
|
||||
effects: [
|
||||
codeMirrorTheme.reconfigure(themeExt),
|
||||
wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []),
|
||||
vimModeComp.reconfigure(vimModeExtension(settingsStore.editorSettings.vimModeEnabled)),
|
||||
runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap)),
|
||||
],
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
|
||||
Reference in new issue
Block a user