fix(editor): let global shortcuts override CodeMirror bindings
This commit is contained in:
1 parent
9051ad35b8
commit
f0cc5aefcf
4 files changed
+110
-2
No files matched your search
@@ -106,6 +106,7 @@ import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, editorDiagnosticC
|
||||
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
|
||||
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
|
||||
import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap";
|
||||
import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap";
|
||||
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
|
||||
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
@@ -521,6 +522,7 @@ let codeMirrorCloseBracketsKeymap: readonly import("@codemirror/view").KeyBindin
|
||||
let readOnlyComp: import("@codemirror/state").Compartment | null = null;
|
||||
let runGutterComp: import("@codemirror/state").Compartment | null = null;
|
||||
let runKeymapComp: import("@codemirror/state").Compartment | null = null;
|
||||
let defaultKeymapComp: 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;
|
||||
@@ -553,6 +555,7 @@ let codeMirrorSelectAll: typeof import("@codemirror/commands").selectAll | null
|
||||
let codeMirrorInsertNewlineKeepIndent: typeof import("@codemirror/commands").insertNewlineKeepIndent | null = null;
|
||||
let codeMirrorToggleLineComment: typeof import("@codemirror/commands").toggleLineComment | null = null;
|
||||
let codeMirrorToggleBlockComment: typeof import("@codemirror/commands").toggleBlockComment | null = null;
|
||||
let codeMirrorDefaultKeymap: readonly import("@codemirror/view").KeyBinding[] | null = null;
|
||||
let codeMirrorToggleFold: typeof import("@codemirror/language").toggleFold | null = null;
|
||||
let pendingCompletionTabTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSemanticDiagnostic[]> | null = null;
|
||||
@@ -4420,6 +4423,11 @@ function refreshCompletionCache() {
|
||||
cachedForeignKeysByTable.clear();
|
||||
}
|
||||
|
||||
function defaultKeymapExtension() {
|
||||
if (!editorViewModule || !codeMirrorDefaultKeymap || !codeMirrorToggleBlockComment) return [];
|
||||
return editorViewModule.keymap.of(defaultKeymapForGlobalShortcuts(codeMirrorDefaultKeymap, settingsStore.editorSettings.shortcuts).filter((item) => item.run !== codeMirrorToggleBlockComment));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!editorRef.value) return;
|
||||
|
||||
@@ -4488,6 +4496,7 @@ onMounted(async () => {
|
||||
readOnlyComp = new Compartment();
|
||||
runGutterComp = new Compartment();
|
||||
runKeymapComp = new Compartment();
|
||||
defaultKeymapComp = new Compartment();
|
||||
completionComp = new Compartment();
|
||||
diagnosticComp = new Compartment();
|
||||
previewRangeComp = new Compartment();
|
||||
@@ -4512,6 +4521,7 @@ onMounted(async () => {
|
||||
codeMirrorInsertNewlineKeepIndent = insertNewlineKeepIndent;
|
||||
codeMirrorToggleLineComment = toggleLineComment;
|
||||
codeMirrorToggleBlockComment = toggleBlockComment;
|
||||
codeMirrorDefaultKeymap = defaultKeymap;
|
||||
codeMirrorToggleFold = toggleFold;
|
||||
codeMirrorIndentUnit = indentUnit;
|
||||
window.addEventListener("keyup", clearTableNavigationHoverOnModifierRelease);
|
||||
@@ -4908,7 +4918,8 @@ onMounted(async () => {
|
||||
activeLineHighlighter,
|
||||
// Vim must be mounted before DBX/default keymaps so normal-mode keys are handled first.
|
||||
vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)),
|
||||
keymap.of([...defaultKeymap.filter((item) => item.run !== toggleBlockComment), ...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymap, ...completionKeymap]),
|
||||
defaultKeymapComp.of(defaultKeymapExtension()),
|
||||
keymap.of([...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymap, ...completionKeymap]),
|
||||
sqlLanguageComp.of(buildSqlLanguageExtension()),
|
||||
sqlSemanticHighlightComp.of(buildSqlSemanticHighlightExtension()),
|
||||
tooltips({ parent: tooltipParent }),
|
||||
@@ -5539,6 +5550,15 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.shortcuts,
|
||||
() => {
|
||||
if (!view.value || !defaultKeymapComp) return;
|
||||
view.value.dispatch({ effects: defaultKeymapComp.reconfigure(defaultKeymapExtension()) });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [settingsStore.editorSettings.sqlFormatter.tabWidth, settingsStore.editorSettings.sqlFormatter.useTabs],
|
||||
() => {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defaultKeymap } from "@codemirror/commands";
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { EditorView, keymap, runScopeHandlers, type KeyBinding } from "@codemirror/view";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultKeymap";
|
||||
import { matchesShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
|
||||
const platforms = [
|
||||
{ name: "macOS", platform: "MacIntel", modifier: "Meta", event: { metaKey: true } },
|
||||
{ name: "Windows", platform: "Win32", modifier: "Ctrl", event: { ctrlKey: true } },
|
||||
{ name: "Linux", platform: "Linux x86_64", modifier: "Ctrl", event: { ctrlKey: true } },
|
||||
] as const;
|
||||
|
||||
function platformIndentBindings(bindings: readonly KeyBinding[], modifier: "Meta" | "Ctrl"): KeyBinding[] {
|
||||
return bindings.filter((binding) => binding.key === "Mod-[" || binding.key === "Mod-]").map((binding) => ({ ...binding, key: binding.key?.replace("Mod", modifier) }));
|
||||
}
|
||||
|
||||
function createView(doc: string, bindings: readonly KeyBinding[], selection?: EditorSelection): EditorView {
|
||||
return new EditorView({
|
||||
parent: document.createElement("div"),
|
||||
state: EditorState.create({
|
||||
doc,
|
||||
selection,
|
||||
extensions: [keymap.of(bindings)],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
describe("defaultKeymapForGlobalShortcuts", () => {
|
||||
it("removes only the bracket binding occupied by a global shortcut", () => {
|
||||
const filtered = defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+[" });
|
||||
expect(filtered.find((binding) => binding.key === "Mod-[")).toBeUndefined();
|
||||
expect(filtered.find((binding) => binding.key === "Mod-]")).toBeDefined();
|
||||
expect(filtered.find((binding) => binding.key === "Mod-a")).toBeDefined();
|
||||
});
|
||||
|
||||
for (const { name, platform, modifier, event } of platforms) {
|
||||
for (const key of ["[", "]"] as const) {
|
||||
it(`${name} lets a conflicting global Mod+${key} action run while the editor is focused`, () => {
|
||||
const shortcut = `Mod+${key}`;
|
||||
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: shortcut }), modifier);
|
||||
const view = createView(" select 1", bindings, EditorSelection.cursor(2));
|
||||
const keyboardEvent = new KeyboardEvent("keydown", { key, ...event });
|
||||
let globalActionRuns = 0;
|
||||
|
||||
if (!runScopeHandlers(view, keyboardEvent, "editor") && matchesShortcut(keyboardEvent, shortcut, platform)) globalActionRuns++;
|
||||
|
||||
expect(globalActionRuns).toBe(1);
|
||||
expect(view.state.doc.toString()).toBe(" select 1");
|
||||
view.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
it(`${name} preserves single-line Mod+[ outdent without a global conflict`, () => {
|
||||
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+B" }), modifier);
|
||||
const view = createView(" select 1", bindings, EditorSelection.cursor(2));
|
||||
|
||||
expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "[", ...event }), "editor")).toBe(true);
|
||||
expect(view.state.doc.toString()).toBe("select 1");
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
it(`${name} preserves multi-line Mod+] indent without a global conflict`, () => {
|
||||
const doc = "select 1\nselect 2";
|
||||
const bindings = platformIndentBindings(defaultKeymapForGlobalShortcuts(defaultKeymap, { toggleSidebar: "Mod+B" }), modifier);
|
||||
const view = createView(doc, bindings, EditorSelection.range(0, doc.length));
|
||||
|
||||
expect(runScopeHandlers(view, new KeyboardEvent("keydown", { key: "]", ...event }), "editor")).toBe(true);
|
||||
expect(view.state.doc.toString()).toBe(" select 1\n select 2");
|
||||
view.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -29,7 +29,8 @@ describe("QueryEditor block comment", () => {
|
||||
expect(queryEditorSource).toContain("shortcut: shortcuts.toggleBlockComment");
|
||||
expect(queryEditorSource).toContain("...binding(shortcuts.toggleBlockComment, (view) => {");
|
||||
expect(queryEditorSource).toContain("!supportsQueryEditorBlockComments(props.databaseType)");
|
||||
expect(queryEditorSource).toContain("defaultKeymap.filter((item) => item.run !== toggleBlockComment)");
|
||||
expect(queryEditorSource).toContain("defaultKeymapForGlobalShortcuts(codeMirrorDefaultKeymap, settingsStore.editorSettings.shortcuts).filter((item) => item.run !== codeMirrorToggleBlockComment)");
|
||||
expect(queryEditorSource).toContain("defaultKeymapComp.reconfigure(defaultKeymapExtension())");
|
||||
expect(queryEditorSource).toContain("codeMirrorToggleBlockComment = toggleBlockComment;");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { KeyBinding } from "@codemirror/view";
|
||||
import { normalizeShortcutSettings, SHORTCUT_DEFINITIONS, shortcutToCodeMirrorKey, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
|
||||
|
||||
const indentBracketKeys = new Set(["Mod-[", "Mod-]"]);
|
||||
const globalShortcutDefinitions = SHORTCUT_DEFINITIONS.filter((definition) => definition.scope === "global");
|
||||
|
||||
export function defaultKeymapForGlobalShortcuts(defaultKeymap: readonly KeyBinding[], shortcuts?: Partial<ShortcutSettings>): KeyBinding[] {
|
||||
const normalizedShortcuts = normalizeShortcutSettings(shortcuts);
|
||||
const occupiedGlobalKeys = new Set(globalShortcutDefinitions.map((definition) => shortcutToCodeMirrorKey(normalizedShortcuts[definition.id])).filter(Boolean));
|
||||
|
||||
return defaultKeymap.filter((binding) => !binding.key || !indentBracketKeys.has(binding.key) || !occupiedGlobalKeys.has(binding.key));
|
||||
}
|
||||
Reference in new issue
Block a user