diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index bc4b16f90..952e402b0 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -164,6 +164,8 @@ let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null; let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion; let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null; +let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null; +let codeMirrorIndentMore: typeof import("@codemirror/commands").indentMore | null = null; let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType | null = null; let semanticDiagnostics: SqlSemanticDiagnostic[] = []; let semanticDiagnosticTimer: ReturnType | null = null; @@ -187,8 +189,6 @@ function syncEditorFontCssVars(fontSize = liveFontSize.value, fontFamily = setti if (!editorRef.value) return; editorRef.value.style.setProperty(EDITOR_FONT_SIZE_CSS_VAR, `${clampEditorFontSize(fontSize)}px`); editorRef.value.style.setProperty(EDITOR_FONT_FAMILY_CSS_VAR, fontFamily); - const cm = editorRef.value.querySelector(".cm-editor") as HTMLElement | null; - if (cm) cm.style.lineHeight = "1.6"; } let pendingFontReconfig: { size: number; family: string } | null = null; @@ -272,6 +272,18 @@ function onEditorGestureEnd(event: Event) { zoomCommitScheduler.flush(liveFontSize.value); } +function handleTab(view: EditorViewType): boolean { + if (codeMirrorCompletionStatus?.(view.state) === "active") return false; + const { state, dispatch } = view; + const sel = state.selection.main; + if (!sel.empty) return codeMirrorIndentMore?.(view) ?? false; + const line = state.doc.lineAt(sel.from); + const before = line.text.slice(0, sel.from - line.from); + if (/^\s*$/.test(before)) return codeMirrorIndentMore?.(view) ?? false; + dispatch(state.update(state.replaceSelection(" "), { userEvent: "input.type" })); + return true; +} + function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view"))["keymap"]) { const shortcuts = settingsStore.editorSettings.shortcuts; return codeMirrorKeymap.of([ @@ -319,6 +331,10 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") return true; }, }, + { + key: shortcutToCodeMirrorKey(shortcuts.acceptCompletion), + run: (view) => codeMirrorAcceptCompletion?.(view) ?? false, + }, ]); } @@ -897,8 +913,16 @@ onMounted(async () => { { EditorState, Compartment, Prec, StateEffect, StateField }, { sql, MSSQL, MySQL, PostgreSQL, SQLDialect }, { basicSetup }, - { autocompletion, startCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus }, - { indentWithTab }, + { + autocompletion, + startCompletion, + acceptCompletion, + closeBrackets, + closeBracketsKeymap, + snippetCompletion, + completionStatus, + }, + { indentMore }, { bracketMatching }, ] = await Promise.all([ import("@codemirror/view"), @@ -919,6 +943,8 @@ onMounted(async () => { diagnosticComp = new Compartment(); setSqlDiagnosticsEffect = StateEffect.define(); codeMirrorCompletionStatus = completionStatus; + codeMirrorAcceptCompletion = acceptCompletion; + codeMirrorIndentMore = indentMore; const diagnosticTheme = EditorView.baseTheme({ ".cm-sql-error": { @@ -1013,7 +1039,7 @@ onMounted(async () => { hoverTooltip((currentView, pos) => resolveSqlHoverTooltip(currentView, pos)), buildSqlSignatureExtension(), diagnosticComp.of(buildSqlDiagnosticExtension()), - Prec.highest(keymap.of([...closeBracketsKeymap, indentWithTab])), + Prec.highest(keymap.of([...closeBracketsKeymap, { key: "Tab", run: handleTab }])), runKeymapComp.of(runKeymapExtension(keymap)), wordWrapComp.of(props.forceWordWrap || ss.wordWrap ? EditorView.lineWrapping : []), readOnlyComp.of([EditorState.readOnly.of(!!props.readOnly), EditorView.editable.of(!props.readOnly)]), diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 560b18640..67c168be9 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1371,6 +1371,7 @@ export default { redisScanPageSizeOption: "{count} keys", shortcutExecuteSql: "Execute SQL", shortcutSaveSql: "Save SQL", + shortcutAcceptCompletion: "Accept completion", shortcutCopyCurrentRow: "Copy current data row", shortcutDeleteCurrentRow: "Delete current data row", shortcutNewQuery: "New query", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 7038e2b99..724f0b7cd 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1253,6 +1253,7 @@ export default { redisScanPageSizeOption: "{count} claves", shortcutExecuteSql: "Ejecutar SQL", shortcutSaveSql: "Guardar SQL", + shortcutAcceptCompletion: "Aceptar completado", shortcutCopyCurrentRow: "Copiar fila de datos actual", shortcutDeleteCurrentRow: "Eliminar fila de datos actual", shortcutNewQuery: "Nueva consulta", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 4246ebc6e..628002335 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1345,6 +1345,7 @@ export default { redisScanPageSizeOption: "{count} 个 Key", shortcutExecuteSql: "执行 SQL", shortcutSaveSql: "保存 SQL", + shortcutAcceptCompletion: "接受补全", shortcutCopyCurrentRow: "复制当前数据行", shortcutDeleteCurrentRow: "删除当前数据行", shortcutNewQuery: "新建查询", diff --git a/apps/desktop/src/lib/editorThemes.ts b/apps/desktop/src/lib/editorThemes.ts index ca860f2de..0df50a3d7 100644 --- a/apps/desktop/src/lib/editorThemes.ts +++ b/apps/desktop/src/lib/editorThemes.ts @@ -102,6 +102,15 @@ export function buildEditorFontThemeRules( ...(opts?.scrollable ? { ".cm-scroller": { overflow: "auto" } } : {}), ".cm-content": { fontFamily: `var(${EDITOR_FONT_FAMILY_CSS_VAR}, ${defaults?.family ?? "monospace"})`, + lineHeight: "1.6", + padding: "0", + }, + ".cm-line": { + padding: "0 2px !important", + }, + ".cm-selectionLayer .cm-selectionBackground": { + transform: "scaleY(1.55)", + transformOrigin: "center", }, ".cm-gutters": { borderRight: "0 !important", diff --git a/apps/desktop/src/lib/keyboardShortcuts.ts b/apps/desktop/src/lib/keyboardShortcuts.ts index 23253d6e3..4d83bc0f4 100644 --- a/apps/desktop/src/lib/keyboardShortcuts.ts +++ b/apps/desktop/src/lib/keyboardShortcuts.ts @@ -96,6 +96,10 @@ export function isSaveShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { + return matchesShortcut(event, actionShortcut("acceptCompletion", shortcuts)); +} + export function isObjectSourceSaveShortcutTarget( target: { closest(selector: string): unknown } | null | undefined, ): boolean { diff --git a/apps/desktop/src/lib/shortcutRegistry.ts b/apps/desktop/src/lib/shortcutRegistry.ts index 21859ef31..6f16c679d 100644 --- a/apps/desktop/src/lib/shortcutRegistry.ts +++ b/apps/desktop/src/lib/shortcutRegistry.ts @@ -1,6 +1,7 @@ export type ShortcutActionId = | "executeSql" | "saveSql" + | "acceptCompletion" | "copyCurrentRow" | "deleteCurrentRow" | "newQuery" @@ -34,6 +35,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "editor", defaultShortcut: "Mod+S", }, + { + id: "acceptCompletion", + labelKey: "settings.shortcutAcceptCompletion", + scope: "editor", + defaultShortcut: "Tab", + }, { id: "copyCurrentRow", labelKey: "settings.shortcutCopyCurrentRow",