diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 16c4769500..0ecbb99110 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -120,7 +120,7 @@ import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recovera import { computePasteCaretResyncTarget } from "@/lib/editor/queryEditorPasteCaretResync"; import { extendQueryEditorSelection, runQueryEditorAltExtendSelection } from "@/lib/editor/queryEditorExtendSelection"; import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation"; -import { isSchemaAware, isSingleDatabase, supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport"; +import { isSchemaAware, isSingleDatabase, supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsQueryEditorBlockComments, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport"; import { metadataSchemaForConnection, sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy"; import { loadTableMetadata } from "@/lib/metadata/tableMetadataCache"; @@ -548,6 +548,7 @@ let codeMirrorRedo: typeof import("@codemirror/commands").redo | null = null; let codeMirrorSelectAll: typeof import("@codemirror/commands").selectAll | null = 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 codeMirrorToggleFold: typeof import("@codemirror/language").toggleFold | null = null; let pendingCompletionTabTimer: ReturnType | null = null; let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType | null = null; @@ -1401,6 +1402,13 @@ function toggleCommentFromContextMenu() { focusEditor(); } +function toggleBlockCommentFromContextMenu() { + const currentView = view.value; + if (!currentView || props.readOnly || !supportsQueryEditorBlockComments(props.databaseType)) return; + codeMirrorToggleBlockComment?.(currentView); + focusEditor(); +} + function selectAllSqlFromContextMenu() { const currentView = view.value; if (!currentView) return; @@ -1709,6 +1717,13 @@ const contextMenuItems = computed(() => { icon: MessageSquareText, shortcut: shortcuts.toggleLineComment, }, + { + label: t("editor.contextMenu.blockCommentSelection"), + action: toggleBlockCommentFromContextMenu, + disabled: props.readOnly || !canCopySelectedSql.value || !supportsQueryEditorBlockComments(props.databaseType), + icon: MessageSquareText, + shortcut: shortcuts.toggleBlockComment, + }, { label: t("editor.contextMenu.formatSelectionSql"), action: () => void formatCurrentSql(), @@ -1880,6 +1895,10 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view") ...binding(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")), ...binding(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")), ...binding(shortcuts.toggleLineComment, (view) => codeMirrorToggleLineComment?.(view) ?? false), + ...binding(shortcuts.toggleBlockComment, (view) => { + if (!supportsQueryEditorBlockComments(props.databaseType)) return false; + return codeMirrorToggleBlockComment?.(view) ?? false; + }), ...binding(shortcuts.toggleFold, (view) => codeMirrorToggleFold?.(view) ?? false), ...binding(shortcuts.exPasteSqlInCondition, () => { if (!supportsSqlInListPaste(props.databaseType)) return false; @@ -4412,7 +4431,7 @@ onMounted(async () => { { EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField }, langSql, { autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion }, - { copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, history, defaultKeymap, historyKeymap }, + { copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, toggleBlockComment, history, defaultKeymap, historyKeymap }, { bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree }, { searchKeymap }, ] = await Promise.all([import("@codemirror/view"), import("@codemirror/state"), import("@codemirror/lang-sql"), import("@codemirror/autocomplete"), import("@codemirror/commands"), import("@codemirror/language"), import("@codemirror/search")]); @@ -4461,6 +4480,7 @@ onMounted(async () => { codeMirrorSelectAll = selectAll; codeMirrorInsertNewlineKeepIndent = insertNewlineKeepIndent; codeMirrorToggleLineComment = toggleLineComment; + codeMirrorToggleBlockComment = toggleBlockComment; codeMirrorToggleFold = toggleFold; codeMirrorIndentUnit = indentUnit; window.addEventListener("keyup", clearTableNavigationHoverOnModifierRelease); @@ -4856,7 +4876,13 @@ 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, ...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymap, ...completionKeymap]), + keymap.of([ + ...defaultKeymap.filter((item) => item.run !== toggleBlockComment), + ...searchKeymapWithoutModD(searchKeymap), + ...historyKeymap, + ...foldKeymap, + ...completionKeymap, + ]), sqlLanguageComp.of(buildSqlLanguageExtension()), sqlSemanticHighlightComp.of(buildSqlSemanticHighlightExtension()), tooltips({ parent: tooltipParent }), diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 783d5a7c30..167e2967dc 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1277,6 +1277,7 @@ export default { exportQueryResultTo: "Export current query result to {format}", copySelection: "Copy selection", commentSelection: "Comment selection", + blockCommentSelection: "Block comment selection", formatSelectionSql: "Format selected SQL", compressSelectionSql: "Compress selected SQL", cutSelection: "Cut selection", @@ -6047,6 +6048,7 @@ export default { shortcutFormatSql: "Format SQL", shortcutExpandSelectStar: "Expand * to columns", shortcutToggleLineComment: "Toggle line comment", + shortcutToggleBlockComment: "Toggle block comment", shortcutIndentMore: "Indent more", shortcutIndentLess: "Indent less", shortcutInsertLineBelow: "Insert line below", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 25f17a9947..ffe7ebb51b 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1255,6 +1255,7 @@ export default withEnglishFallback({ exportQueryResultTo: "Exportar el conjunto de resultados de la consulta actual a {format}", copySelection: "Copiar seleccion", commentSelection: "Comentar selección", + blockCommentSelection: "Comentario de bloque en la selección", formatSelectionSql: "Formatear SQL seleccionado", compressSelectionSql: "Comprimir SQL seleccionado", sendToAi: "Enviar a IA", @@ -5748,6 +5749,7 @@ export default withEnglishFallback({ shortcutFormatSql: "Formatear SQL", shortcutExpandSelectStar: "Expandir * a columnas", shortcutToggleLineComment: "Alternar comentario de línea", + shortcutToggleBlockComment: "Alternar comentario de bloque", shortcutIndentMore: "Aumentar sangría", shortcutIndentLess: "Reducir sangría", shortcutInsertLineBelow: "Insertar una línea debajo", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 8a203445d0..264a7a08d4 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1253,6 +1253,7 @@ export default withEnglishFallback({ exportQueryResultTo: "Esporta il set di risultati della query corrente in {format}", copySelection: "Copia selezione", commentSelection: "Commenta selezione", + blockCommentSelection: "Commenta selezione a blocco", formatSelectionSql: "Formatta SQL selezionato", compressSelectionSql: "Comprimi SQL selezionato", sendToAi: "Invia ad AI", @@ -5748,6 +5749,7 @@ export default withEnglishFallback({ shortcutFormatSql: "Formatta SQL", shortcutExpandSelectStar: "Espandi * in colonne", shortcutToggleLineComment: "Attiva/disattiva commento di riga", + shortcutToggleBlockComment: "Attiva/disattiva commento a blocco", shortcutIndentMore: "Aumenta rientro", shortcutIndentLess: "Riduci rientro", shortcutInsertLineBelow: "Inserisci una riga sotto", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 54453e19b8..a33f49280c 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1273,6 +1273,7 @@ export default withEnglishFallback({ exportQueryResultTo: "現在のクエリ結果セットを {format} にエクスポート", copySelection: "選択範囲をコピー", commentSelection: "選択範囲をコメント化", + blockCommentSelection: "選択範囲をブロックコメント化", formatSelectionSql: "選択した SQL をフォーマット", compressSelectionSql: "選択した SQL を圧縮", sendToAi: "AIに送信", @@ -5763,6 +5764,7 @@ export default withEnglishFallback({ shortcutFormatSql: "SQLをフォーマット", shortcutExpandSelectStar: "* を列に展開", shortcutToggleLineComment: "行コメントを切り替え", + shortcutToggleBlockComment: "ブロックコメントを切り替え", shortcutIndentMore: "インデントを増やす", shortcutIndentLess: "インデントを減らす", shortcutInsertLineBelow: "下に行を挿入", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index bc0855e7d6..be4d9ff0d8 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1164,6 +1164,7 @@ export default withEnglishFallback({ exportQueryResultTo: "현재 쿼리 결과를 {format}(으)로 내보내기", copySelection: "선택 영역 복사", commentSelection: "선택 항목 주석 처리", + blockCommentSelection: "선택 항목 블록 주석 처리", formatSelectionSql: "선택한 SQL 서식 지정", compressSelectionSql: "선택한 SQL 압축", cutSelection: "선택 영역 잘라내기", @@ -5506,6 +5507,7 @@ export default withEnglishFallback({ shortcutFormatSql: "SQL 서식", shortcutExpandSelectStar: "*을(를) 열로 확장", shortcutToggleLineComment: "줄 주석 전환", + shortcutToggleBlockComment: "블록 주석 전환", shortcutIndentMore: "들여쓰기 늘리기", shortcutIndentLess: "들여쓰기 줄이기", shortcutInsertLineBelow: "아래에 줄 삽입", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index fc677d614d..6c912de98c 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1254,6 +1254,7 @@ export default withEnglishFallback({ exportQueryResultTo: "Exportar conjunto de resultados da consulta atual para {format}", copySelection: "Copiar seleção", commentSelection: "Comentar seleção", + blockCommentSelection: "Comentário de bloco na seleção", formatSelectionSql: "Formatar SQL selecionado", compressSelectionSql: "Compactar SQL selecionado", sendToAi: "Enviar para IA", @@ -5750,6 +5751,7 @@ export default withEnglishFallback({ shortcutFormatSql: "Formatar SQL", shortcutExpandSelectStar: "Expandir * em colunas", shortcutToggleLineComment: "Alternar comentário de linha", + shortcutToggleBlockComment: "Alternar comentário de bloco", shortcutIndentMore: "Aumentar recuo", shortcutIndentLess: "Reduzir recuo", shortcutInsertLineBelow: "Inserir uma linha abaixo", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 83ab72585b..94eb4168e8 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1201,6 +1201,7 @@ export default withEnglishFallback({ exportQueryResultTo: "导出当前查询结果集到 {format}", copySelection: "复制选中内容", commentSelection: "注释选中内容", + blockCommentSelection: "块注释选中内容", formatSelectionSql: "格式化选中SQL", compressSelectionSql: "压缩选中SQL", cutSelection: "剪切选中内容", @@ -6033,6 +6034,7 @@ export default withEnglishFallback({ shortcutFormatSql: "格式化 SQL", shortcutExpandSelectStar: "将 * 展开为字段", shortcutToggleLineComment: "切换行注释", + shortcutToggleBlockComment: "切换块注释", shortcutIndentMore: "增加缩进", shortcutIndentLess: "减少缩进", shortcutInsertLineBelow: "在下方新增一行", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 12ae6668c2..047cddb94a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1253,6 +1253,7 @@ export default withEnglishFallback({ exportQueryResultTo: "匯出目前查詢結果集到 {format}", copySelection: "複製選取內容", commentSelection: "註解選取內容", + blockCommentSelection: "區塊註解選取內容", formatSelectionSql: "格式化選取的 SQL", compressSelectionSql: "壓縮選取的 SQL", sendToAi: "傳送至 AI", @@ -5074,6 +5075,7 @@ export default withEnglishFallback({ shortcutFormatSql: "格式化 SQL", shortcutExpandSelectStar: "將 * 展開為欄位", shortcutToggleLineComment: "切換行註解", + shortcutToggleBlockComment: "切換區塊註解", shortcutIndentMore: "增加縮排", shortcutIndentLess: "減少縮排", shortcutInsertLineBelow: "在下方新增一行", diff --git a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts index 6af990556c..454fb499bd 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts @@ -9,6 +9,7 @@ import { supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsQueryTargetDatabaseListing, + supportsQueryEditorBlockComments, supportsSqlInListPaste, supportsTableImport, supportsTransaction, @@ -123,6 +124,21 @@ describe("supportsSqlInListPaste", () => { }); }); +describe("supportsQueryEditorBlockComments", () => { + it("allows block comments in generic and SQL editors", () => { + expect(supportsQueryEditorBlockComments(undefined)).toBe(true); + expect(supportsQueryEditorBlockComments("mysql")).toBe(true); + expect(supportsQueryEditorBlockComments("postgres")).toBe(true); + expect(supportsQueryEditorBlockComments("sqlserver")).toBe(true); + }); + + it("hides block comments in non-SQL editors", () => { + expect(supportsQueryEditorBlockComments("redis")).toBe(false); + expect(supportsQueryEditorBlockComments("mongodb")).toBe(false); + expect(supportsQueryEditorBlockComments("elasticsearch")).toBe(false); + }); +}); + describe("supportsTableImport", () => { it("enables OceanBase Oracle table import", () => { expect(supportsTableImport("oceanbase-oracle")).toBe(true); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorBlockComment.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorBlockComment.spec.ts new file mode 100644 index 0000000000..7ff0b8aa27 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorBlockComment.spec.ts @@ -0,0 +1,52 @@ +import { readFileSync } from "node:fs"; +import { toggleBlockComment } from "@codemirror/commands"; +import { sql } from "@codemirror/lang-sql"; +import { EditorSelection, EditorState, type Transaction } from "@codemirror/state"; +import { describe, expect, it, vi } from "vitest"; + +const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8"); + +function runToggleBlockComment(doc: string, selection: EditorSelection | { anchor: number; head?: number }) { + let state = EditorState.create({ doc, selection, extensions: [sql()] }); + const dispatch = vi.fn((transaction: Transaction) => { + state = transaction.state; + }); + const handled = toggleBlockComment({ + get state() { + return state; + }, + dispatch, + } as never); + + return { dispatch, handled, state }; +} + +describe("QueryEditor block comment", () => { + it("wires toggleBlockComment into the right-click context menu, the keymap, and the editor module load", () => { + expect(queryEditorSource).toContain('let codeMirrorToggleBlockComment: typeof import("@codemirror/commands").toggleBlockComment | null = null;'); + expect(queryEditorSource).toContain("codeMirrorToggleBlockComment?.(currentView);"); + expect(queryEditorSource).toContain('label: t("editor.contextMenu.blockCommentSelection")'); + 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("codeMirrorToggleBlockComment = toggleBlockComment;"); + }); + + it("wraps the selected SQL in a block comment", () => { + const doc = "SELECT id FROM users;"; + const selection = EditorSelection.range(doc.indexOf("id"), doc.indexOf("id") + "id".length); + const result = runToggleBlockComment(doc, selection); + + expect(result.handled).toBe(true); + expect(result.state.doc.toString()).toBe("SELECT /* id */ FROM users;"); + }); + + it("removes an existing block comment around the selection", () => { + const doc = "SELECT /* id */ FROM users;"; + const selection = EditorSelection.range(doc.indexOf("/*"), doc.indexOf("*/") + "*/".length); + const result = runToggleBlockComment(doc, selection); + + expect(result.state.doc.toString()).toBe("SELECT id FROM users;"); + }); +}); diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index ab559dff08..8b537ff41b 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -5,6 +5,7 @@ describe("shortcutRegistry editor actions", () => { const formatterEditorActionIds: ShortcutActionId[] = [ "formatSql", "toggleLineComment", + "toggleBlockComment", "indentMore", "indentLess", "joinLines", @@ -126,6 +127,7 @@ describe("shortcutRegistry editor actions", () => { expect(shortcuts.executeSql).toBe("Mod+Shift+Enter"); expect(shortcuts.formatSql).toBe("Shift+Mod+F"); expect(shortcuts.toggleLineComment).toBe("Mod+/"); + expect(shortcuts.toggleBlockComment).toBe("Shift+Alt+A"); expect(shortcuts.indentMore).toBe(""); expect(shortcuts.indentLess).toBe("Shift+Tab"); expect(shortcuts.joinLines).toBe("Mod+J"); diff --git a/apps/desktop/src/lib/database/databaseFeatureSupport.ts b/apps/desktop/src/lib/database/databaseFeatureSupport.ts index a21cae20d0..860bf49f3a 100644 --- a/apps/desktop/src/lib/database/databaseFeatureSupport.ts +++ b/apps/desktop/src/lib/database/databaseFeatureSupport.ts @@ -131,6 +131,11 @@ export function supportsSqlInListPaste(dbType?: DatabaseType): boolean { return supportsSqlFileExecution(dbType) && !NON_SQL_IN_LIST_PASTE_TYPES.has(dbType); } +export function supportsQueryEditorBlockComments(dbType?: DatabaseType): boolean { + if (!dbType) return true; + return supportsSqlFileExecution(dbType); +} + export function supportsSchemaDiagram(dbType?: DatabaseType): boolean { return supportsDatabaseFeature(dbType, "diagram"); } diff --git a/apps/desktop/src/lib/editor/shortcutRegistry.ts b/apps/desktop/src/lib/editor/shortcutRegistry.ts index 948d0dd5e8..4cceeb07a6 100644 --- a/apps/desktop/src/lib/editor/shortcutRegistry.ts +++ b/apps/desktop/src/lib/editor/shortcutRegistry.ts @@ -6,6 +6,7 @@ export type ShortcutActionId = | "formatSql" | "expandSelectStar" | "toggleLineComment" + | "toggleBlockComment" | "saveSql" | "acceptCompletion" | "indentMore" @@ -119,6 +120,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "editor", defaultShortcut: "Mod+/", }, + { + id: "toggleBlockComment", + labelKey: "settings.shortcutToggleBlockComment", + scope: "editor", + defaultShortcut: "Shift+Alt+A", + }, { id: "saveSql", labelKey: "settings.shortcutSaveSql",