feat(editor): support customizing snippet trigger key

This commit is contained in:
lxk955
2026-10-04 09:57:49 +08:00
committed by GitHub
parent b10ecd096c
commit 69687c1a84
25 changed files with 242 additions and 10 deletions
@@ -114,6 +114,7 @@ import {
normalizeMcpGlobalPolicy,
type ClickTableNavigationTarget,
type EditorSettings,
type SnippetTriggerKey,
type SqlCompletionTriggerMode,
type SqlTableCompletionSchemaQualification,
type TableHoverLookupMode,
@@ -967,6 +968,7 @@ function editableSnippet(snippet: SqlSnippet): SqlSnippet {
}
const editSnippets = ref<SqlSnippet[]>(settingsStore.editorSettings.snippets.map(editableSnippet));
const editSnippetTriggerKey = ref<SnippetTriggerKey>(settingsStore.editorSettings.snippetTriggerKey);
function editableSqlShortcut(action: SqlShortcutAction): SqlShortcutAction {
const next: SqlShortcutAction = { ...action, enabled: action.enabled !== false };
@@ -1179,6 +1181,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
updateDownloadSource: editUpdateDownloadSource.value,
toolbarItems: { ...editToolbarItems.value },
snippets: editSnippets.value,
snippetTriggerKey: editSnippetTriggerKey.value,
sqlShortcuts: editSqlShortcuts.value,
sqlVariableSubstitutionEnabled: editSqlVariableSubstitutionEnabled.value,
sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides.value,
@@ -1851,6 +1854,7 @@ function syncEditorSettingsDraftFromStore() {
editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource;
editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems };
editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet);
editSnippetTriggerKey.value = settingsStore.editorSettings.snippetTriggerKey;
editSqlShortcuts.value = mergeDefaultSqlShortcuts(settingsStore.editorSettings.sqlShortcuts.map(editableSqlShortcut));
editSqlVariableSubstitutionEnabled.value = settingsStore.editorSettings.sqlVariableSubstitutionEnabled;
editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides);
@@ -1997,6 +2001,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
updateDownloadSource: editUpdateDownloadSource,
toolbarItems: editToolbarItems,
snippets: editSnippets,
snippetTriggerKey: editSnippetTriggerKey,
sqlShortcuts: editSqlShortcuts,
sqlVariableSubstitutionEnabled: editSqlVariableSubstitutionEnabled,
sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides,
@@ -2544,6 +2549,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
} else if (tab === "snippets") {
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
editSnippetTriggerKey.value = DEFAULT_EDITOR_SETTINGS.snippetTriggerKey;
} else if (tab === "updates") {
editAutoUpdateApp.value = DEFAULT_EDITOR_SETTINGS.autoUpdateApp;
editAutoUpdateDrivers.value = DEFAULT_EDITOR_SETTINGS.autoUpdateDrivers;
@@ -2694,6 +2700,7 @@ function resetAllDefaults() {
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
editSnippetTriggerKey.value = DEFAULT_EDITOR_SETTINGS.snippetTriggerKey;
editSqlShortcuts.value = DEFAULT_SQL_SHORTCUTS.map(editableSqlShortcut);
}
@@ -2835,6 +2842,12 @@ function onCompletionTriggerModeChange(v: any) {
}
}
function onSnippetTriggerKeyChange(v: any) {
if (v === "tab" || v === "space" || v === "both") {
editSnippetTriggerKey.value = v;
}
}
function onTableCompletionSchemaQualificationChange(v: any) {
if (v === "never" || v === "collision" || v === "always") {
editTableCompletionSchemaQualification.value = v;
@@ -9395,6 +9408,26 @@ onUnmounted(() => {
{{ t("settings.snippetsAdd") }}
</Button>
</div>
<div data-settings-search-id="snippet-trigger-key" :class="['settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('snippet-trigger-key')]">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.snippetTriggerKey") }}</Label>
<p class="text-xs leading-tight text-muted-foreground">
{{ t("settings.snippetTriggerKeyDescription") }}
</p>
</div>
<Select :model-value="editSnippetTriggerKey" @update:model-value="onSnippetTriggerKeyChange">
<SelectTrigger class="h-8 w-44 shrink-0">
<SelectValue :placeholder="t('settings.snippetTriggerKey')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tab">{{ t("settings.snippetTriggerKeyTab") }}</SelectItem>
<SelectItem value="space">{{ t("settings.snippetTriggerKeySpace") }}</SelectItem>
<SelectItem value="both">{{ t("settings.snippetTriggerKeyBoth") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<p>{{ t("settings.snippetsPlaceholderHint") }}</p>
<pre class="mt-2 overflow-x-auto whitespace-pre-wrap rounded bg-background/70 px-2 py-1.5 font-mono text-[11px] leading-relaxed text-foreground">
@@ -1812,7 +1812,7 @@ const completion = useQueryEditorCompletion({
});
const { triggerSqlCompletion, shouldTriggerSqlCompletionForPosition, scheduleSqlCompletionStart, consumeSqlCompletionAutoStartSuppression, scheduleDeferredCompletionTrigger, clearDeferredCompletionTrigger } = completion;
const { editorIndentUnit, handleTab, handleEnter, acceptCompletionOrNextSnippetField, acceptSqlServerCompletionOnSpace, clearPendingCompletionEnter, clearPendingCompletionTab } = useQueryEditorCompletionKeys({
const { editorIndentUnit, handleTab, handleEnter, acceptCompletionOrNextSnippetField, handleSpace, clearPendingCompletionEnter, clearPendingCompletionTab } = useQueryEditorCompletionKeys({
props,
settingsStore,
runtime: codeMirrorRuntime,
@@ -1998,7 +1998,7 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
initializedRuntime.vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)),
initializedRuntime.defaultKeymapComp.of(defaultKeymapExtension()),
keymap.of([...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymapWithoutAllBindings(foldKeymap, initializedRuntime.codeMirrorFoldAll, initializedRuntime.codeMirrorUnfoldAll), ...completionKeymap]),
Prec.highest(keymap.of([{ key: "Space", run: acceptSqlServerCompletionOnSpace }])),
Prec.highest(keymap.of([{ key: "Space", run: handleSpace }])),
initializedRuntime.sqlLanguageComp.of(sqlExtensions.buildSqlLanguageExtension()),
initializedRuntime.sqlSemanticHighlightComp.of(sqlExtensions.buildSqlSemanticHighlightExtension()),
createSqlUnknownObjectHighlights({
@@ -33,4 +33,10 @@ describe("EditorSettingsDialog settings search targets", () => {
expect(entry?.category).toBe(owningTab);
expect(dialogSource).toContain(`data-settings-search-id="${entry?.targetId}"`);
});
it("renders the snippet trigger key setting in the tab its search entry points at", () => {
const entry = SETTINGS_SEARCH_DEFINITIONS.find((definition) => definition.id === "snippet-trigger-key");
expect(entry?.category).toBe("snippets");
expect(dialogSource).toContain(`data-settings-search-id="${entry?.targetId}"`);
});
});
@@ -45,7 +45,7 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
codeMirrorSelectFirstCompletion: vi.fn(() => false),
codeMirrorInsertNewlineKeepIndent: newline,
});
const settings = reactive({ editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 }, shortcuts: { acceptCompletion: "Tab" }, selectFirstCompletionOnOpen: false, sqlServerSpaceConfirmsCompletion: true } });
const settings = reactive({ editorSettings: { sqlFormatter: { useTabs: false, tabWidth: 2 }, shortcuts: { acceptCompletion: "Tab" }, selectFirstCompletionOnOpen: false, sqlServerSpaceConfirmsCompletion: true, snippetTriggerKey: "tab" as const } });
const completion = { suppressAutoStartUntil: 0 };
const keys = useQueryEditorCompletionKeys({ props, settingsStore: settings as Options["settingsStore"], runtime, completion, batchSelection: { applySelectedBatchColumnSelection: batch }, isEditorComposing: composing, retryDelayMs: 5, tabMaxWaitMs: 40, enterMaxWaitMs: 20 });
cleanups.push(() => {
@@ -174,4 +174,69 @@ describe("QueryEditor completion-key ownership", () => {
expect(view.state.doc.toString()).toBe("users ");
expect(view.state.selection.main.head).toBe(6);
});
it("accepts snippet completion on Tab when snippetTriggerKey is tab or both", () => {
const { keys, view, status, runtime, accept } = createHarness();
status.mockReturnValue("active");
runtime.codeMirrorSelectedCompletionIndex = () => 0;
runtime.codeMirrorSelectedCompletion = () => ({ label: "sel", type: "snippet" });
accept.mockReturnValue(true);
expect(keys.handleTab(view)).toBe(true);
expect(accept).toHaveBeenCalledOnce();
});
it("does not accept snippet completion on Tab when snippetTriggerKey is space", () => {
const { keys, view, status, runtime, accept, settings } = createHarness();
settings.editorSettings.snippetTriggerKey = "space";
status.mockReturnValue("active");
runtime.codeMirrorSelectedCompletionIndex = () => 0;
runtime.codeMirrorSelectedCompletion = () => ({ label: "sel", type: "snippet" });
accept.mockReturnValue(true);
expect(keys.handleTab(view)).toBe(true);
expect(accept).not.toHaveBeenCalled();
expect(view.state.doc.toString()).toBe("abc ");
});
it("accepts snippet completion on Space when snippetTriggerKey is space or both", () => {
const { keys, view, status, runtime, accept, settings } = createHarness();
settings.editorSettings.snippetTriggerKey = "space";
status.mockReturnValue("active");
runtime.codeMirrorSelectedCompletionIndex = () => 0;
runtime.codeMirrorSelectedCompletion = () => ({ label: "sel", type: "snippet" });
accept.mockReturnValue(true);
expect(keys.handleSpace(view)).toBe(true);
expect(accept).toHaveBeenCalledOnce();
accept.mockClear();
settings.editorSettings.snippetTriggerKey = "both";
expect(keys.handleSpace(view)).toBe(true);
expect(accept).toHaveBeenCalledOnce();
});
it("does not accept snippet completion on Space when snippetTriggerKey is tab", () => {
const { keys, view, status, runtime, accept, settings } = createHarness();
settings.editorSettings.snippetTriggerKey = "tab";
status.mockReturnValue("active");
runtime.codeMirrorSelectedCompletionIndex = () => 0;
runtime.codeMirrorSelectedCompletion = () => ({ label: "sel", type: "snippet" });
accept.mockReturnValue(true);
expect(keys.handleSpace(view)).toBe(false);
expect(accept).not.toHaveBeenCalled();
});
it("ignores Space for snippet completion while an IME composition is active", () => {
const { keys, view, status, runtime, accept, composing, settings } = createHarness();
settings.editorSettings.snippetTriggerKey = "space";
status.mockReturnValue("active");
runtime.codeMirrorSelectedCompletionIndex = () => 0;
runtime.codeMirrorSelectedCompletion = () => ({ label: "sel", type: "snippet" });
composing.mockReturnValue(true);
expect(keys.handleSpace(view)).toBe(false);
expect(accept).not.toHaveBeenCalled();
});
});
@@ -129,6 +129,24 @@ export function useQueryEditorCompletionKeys(options: QueryEditorCompletionKeysO
return handled;
}
function getTargetCompletion(view: EditorViewType): QueryCompletionOption | null {
const selected = codeMirrorRuntime.codeMirrorSelectedCompletion?.(view.state) as QueryCompletionOption | null | undefined;
if (selected) return selected;
const current = codeMirrorRuntime.codeMirrorCurrentCompletions?.(view.state) as QueryCompletionOption[] | undefined;
if (current && current.length > 0) {
return current[0] ?? null;
}
return null;
}
function canAcceptCurrentCompletionOnTab(view: EditorViewType): boolean {
if (settingsStore.editorSettings.snippetTriggerKey === "space") {
const target = getTargetCompletion(view);
if (target?.type === "snippet") return false;
}
return true;
}
function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
// A non-empty selection belongs to snippet navigation or block indentation,
// not word completion. A popup opened by an indent edit must not hijack Tab.
@@ -136,12 +154,26 @@ export function useQueryEditorCompletionKeys(options: QueryEditorCompletionKeysO
if (view.state.selection.ranges.every((range) => range.empty)) {
const completionStatus = codeMirrorRuntime.codeMirrorCompletionStatus?.(view.state) ?? null;
if (isBatchColumnSelectionCompletionActive(completionStatus) && applySelectedBatchColumnSelection(view)) return true;
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorRuntime.codeMirrorAcceptCompletion, codeMirrorRuntime.codeMirrorSelectedCompletionIndex, codeMirrorRuntime.codeMirrorSelectFirstCompletion)) return true;
if (completionStatus) return waitForCompletionTab(view);
if (completionStatus === "active" && canAcceptCurrentCompletionOnTab(view) && acceptSelectedOrFirstCompletion(view, codeMirrorRuntime.codeMirrorAcceptCompletion, codeMirrorRuntime.codeMirrorSelectedCompletionIndex, codeMirrorRuntime.codeMirrorSelectFirstCompletion)) return true;
if (completionStatus && canAcceptCurrentCompletionOnTab(view)) return waitForCompletionTab(view);
}
return codeMirrorRuntime.codeMirrorNextSnippetField?.(view) ?? false;
}
function acceptSnippetCompletionOnSpace(view: EditorViewType): boolean {
if (isEditorComposing(view)) return false;
const triggerKey = settingsStore.editorSettings.snippetTriggerKey;
if (triggerKey !== "space" && triggerKey !== "both") return false;
if (codeMirrorRuntime.codeMirrorCompletionStatus?.(view.state) !== "active") return false;
if (!view.state.selection.main.empty) return false;
const target = getTargetCompletion(view);
if (target?.type !== "snippet") return false;
if (target?.dbxBatchColumnSelection || target?.dbxBatchColumnSelectionAction) return false;
return acceptSelectedOrFirstCompletion(view, codeMirrorRuntime.codeMirrorAcceptCompletion, codeMirrorRuntime.codeMirrorSelectedCompletionIndex, codeMirrorRuntime.codeMirrorSelectFirstCompletion);
}
function acceptSqlServerCompletionOnSpace(view: EditorViewType): boolean {
if (isEditorComposing(view)) return false;
if (props.databaseType !== "sqlserver" || !settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion) return false;
@@ -174,6 +206,13 @@ export function useQueryEditorCompletionKeys(options: QueryEditorCompletionKeysO
return true;
}
function handleSpace(view: EditorViewType): boolean {
if (isEditorComposing(view)) return false;
if (!view.state.selection.main.empty) return false;
if (acceptSnippetCompletionOnSpace(view)) return true;
return acceptSqlServerCompletionOnSpace(view);
}
function clearPendingCompletionTab() {
if (pendingCompletionTabTimer === null) return;
clearTimeout(pendingCompletionTabTimer);
@@ -196,8 +235,8 @@ export function useQueryEditorCompletionKeys(options: QueryEditorCompletionKeysO
if (view.state.doc !== initialDoc || selectionRanges.length !== initialSelectionRanges.length || selectionRanges.some((range, index) => !range.empty || range.anchor !== initialSelectionRanges[index]?.anchor || range.head !== initialSelectionRanges[index]?.head)) return;
const completionStatus = codeMirrorRuntime.codeMirrorCompletionStatus?.(view.state) ?? null;
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorRuntime.codeMirrorAcceptCompletion, codeMirrorRuntime.codeMirrorSelectedCompletionIndex, codeMirrorRuntime.codeMirrorSelectFirstCompletion)) return;
if (completionStatus && Date.now() - startedAt < COMPLETION_TAB_MAX_WAIT_MS) {
if (completionStatus === "active" && canAcceptCurrentCompletionOnTab(view) && acceptSelectedOrFirstCompletion(view, codeMirrorRuntime.codeMirrorAcceptCompletion, codeMirrorRuntime.codeMirrorSelectedCompletionIndex, codeMirrorRuntime.codeMirrorSelectFirstCompletion)) return;
if (completionStatus && Date.now() - startedAt < COMPLETION_TAB_MAX_WAIT_MS && canAcceptCurrentCompletionOnTab(view)) {
pendingCompletionTabTimer = setTimeout(retry, COMPLETION_TAB_RETRY_DELAY_MS);
return;
}
@@ -212,5 +251,5 @@ export function useQueryEditorCompletionKeys(options: QueryEditorCompletionKeysO
return true;
}
return { editorIndentUnit, handleTab, handleEnter, acceptCompletionOrNextSnippetField, acceptSqlServerCompletionOnSpace, clearPendingCompletionEnter, clearPendingCompletionTab };
return { editorIndentUnit, handleTab, handleEnter, acceptCompletionOrNextSnippetField, acceptSqlServerCompletionOnSpace, acceptSnippetCompletionOnSpace, handleSpace, clearPendingCompletionEnter, clearPendingCompletionTab };
}
+5
View File
@@ -7382,6 +7382,11 @@ export default withEnglishFallback({
sidebarFontSize: "Yan panel şriftinin ölçüsü",
sidebarFontSizeDescription: "Yan paneldəki obyekt ağacının şrift ölçüsü, piksellə. Redaktorun şrift ölçüsündən asılı deyil.",
snippetsDescription: "Redaktorda işə salınan SQL kod parçası şablonlarını fərdiləşdirin.",
snippetTriggerKey: "Kod parçası işə salma düyməsi",
snippetTriggerKeyDescription: "Redaktorda kod parçasının genişləndirilməsini işə salmaq üçün istifadə olunan düyməni seçin (Tab, Boşluq və ya Hər ikisi).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Boşluq",
snippetTriggerKeyBoth: "Tab və ya Boşluq",
snippetsPlaceholderHint: "Redaktə edilə bilən yer tutucu yaratmaq üçün {'${number:default text}'} sintaksisindən istifadə edin. Eyni nömrəli yer tutucular əlaqəli qalır; məsələn:",
snippetsAdd: "Kod parçası əlavə et",
snippetsLabel: "Etiket",
+5
View File
@@ -8276,6 +8276,11 @@ export default {
sidebarFontSize: "Sidebar font size",
sidebarFontSizeDescription: "Font size for the sidebar object tree, in pixels. Independent of the editor font size.",
snippetsDescription: "Customize SQL snippet templates triggered in the editor.",
snippetTriggerKey: "Snippet trigger key",
snippetTriggerKeyDescription: "Choose the key used to trigger snippet expansion in the editor (Tab, Space, or Both).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Space",
snippetTriggerKeyBoth: "Tab or Space",
snippetsPlaceholderHint: "Use {'${number:default text}'} to create an editable placeholder. Placeholders with the same number stay linked; for example:",
snippetsAdd: "Add Snippet",
snippetsLabel: "Label",
+5
View File
@@ -7599,6 +7599,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "Sugerencias al pasar el cursor en la barra lateral",
sidebarShowTooltipsDescription: "Muestra sugerencias con detalles al pasar el cursor sobre conexiones y objetos de la base de datos en la barra lateral. Desactívalo para ocultarlas.",
snippetsDescription: "Personaliza plantillas SQL activadas en el editor.",
snippetTriggerKey: "Tecla de activación de fragmentos",
snippetTriggerKeyDescription: "Elija la tecla utilizada para activar la expansión de fragmentos en el editor (Tab, Espacio o Ambos).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Espacio",
snippetTriggerKeyBoth: "Tab o Espacio",
syncSnippetProviderGitee: "Fragmentos de Gitee",
syncGitLabInstance: "URL de la instancia de GitLab",
syncGitLabInstanceInvalid: "Introduce una URL HTTP o HTTPS sin credenciales, parámetros de consulta ni fragmentos.",
+5
View File
@@ -8069,6 +8069,11 @@ export default withEnglishFallback({
sidebarFontSize: "Ukuran font bilah sisi",
sidebarFontSizeDescription: "Ukuran font untuk pohon objek bilah sisi, dalam piksel. Independen dari ukuran font editor.",
snippetsDescription: "Sesuaikan template snippet SQL yang dipicu di editor.",
snippetTriggerKey: "Tombol pemicu snippet",
snippetTriggerKeyDescription: "Pilih tombol yang digunakan untuk memicu perluasan snippet di editor (Tab, Spasi, atau Keduanya).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Spasi",
snippetTriggerKeyBoth: "Tab atau Spasi",
snippetsPlaceholderHint: "Gunakan {'${number:default text}'} untuk membuat placeholder yang dapat diedit. Placeholder dengan nomor yang sama akan tetap tertaut; contohnya:",
snippetsAdd: "Tambah Snippet",
snippetsLabel: "Label",
+5
View File
@@ -7377,6 +7377,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "Tooltip al passaggio del mouse nella barra laterale",
sidebarShowTooltipsDescription: "Mostra tooltip con dettagli al passaggio del mouse su connessioni e oggetti del database nella barra laterale. Disattiva per nasconderli.",
snippetsDescription: "Personalizza i modelli di snippet SQL attivati nell'editor.",
snippetTriggerKey: "Tasto di attivazione dello snippet",
snippetTriggerKeyDescription: "Scegli il tasto utilizzato per attivare l'espansione dello snippet nell'editor (Tab, Spazio o Entrambi).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Spazio",
snippetTriggerKeyBoth: "Tab o Spazio",
syncSnippetProviderGitee: "Snippet Gitee",
syncGitLabInstance: "URL dell'istanza GitLab",
syncGitLabInstanceInvalid: "Inserisci un URL HTTP o HTTPS senza credenziali, parametri di query o frammenti.",
+5
View File
@@ -7392,6 +7392,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "サイドバーのホバーツールチップ",
sidebarShowTooltipsDescription: "サイドバーで接続やデータベースオブジェクトにホバーしたときに詳細ツールチップを表示します。オフにすると表示されません。",
snippetsDescription: "エディタでトリガーされるSQLスニペットテンプレートをカスタマイズします。",
snippetTriggerKey: "スニペットトリガーキー",
snippetTriggerKeyDescription: "エディタでスニペットを展開するトリガーキーを設定します(Tab、スペース、または両方)。",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "スペース",
snippetTriggerKeyBoth: "Tab またはスペース",
syncSnippetProviderGitee: "Gitee コードスニペット",
syncGitLabInstance: "GitLab インスタンス URL",
syncGitLabInstanceInvalid: "認証情報、クエリ、フラグメントを含まない HTTP または HTTPS の URL を入力してください。",
+5
View File
@@ -7178,6 +7178,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "사이드바 호버 툴팁",
sidebarShowTooltipsDescription: "사이드바에서 연결 및 데이터베이스 개체에 마우스를 올리면 상세 툴팁을 표시합니다. 끄면 표시되지 않습니다.",
snippetsDescription: "편집기에서 트리거되는 SQL 스니펫 템플릿을 사용자 정의합니다.",
snippetTriggerKey: "스니펫 트리거 키",
snippetTriggerKeyDescription: "편집기에서 스니펫 확장을 트리거하는 키를 설정합니다(Tab, 스페이스 또는 둘 다).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "스페이스",
snippetTriggerKeyBoth: "Tab 또는 스페이스",
snippetsPlaceholderHint: "{'${번호:기본 텍스트}'}를 사용하여 편집 가능한 자리 표시자를 만듭니다. 같은 번호의 자리 표시자는 함께 수정됩니다. 예:",
snippetsAdd: "스니펫 추가",
snippetsLabel: "라벨",
+5
View File
@@ -7379,6 +7379,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "Dicas ao passar o mouse na barra lateral",
sidebarShowTooltipsDescription: "Mostra dicas com detalhes ao passar o mouse sobre conexões e objetos do banco de dados na barra lateral. Desative para ocultá-las.",
snippetsDescription: "Personalize os modelos de snippets SQL acionados no editor.",
snippetTriggerKey: "Tecla de ativação de snippets",
snippetTriggerKeyDescription: "Escolha a tecla usada para acionar a expansão de snippets no editor (Tab, Espaço ou Ambos).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Espaço",
snippetTriggerKeyBoth: "Tab ou Espaço",
syncSnippetProviderGitee: "Snippets do Gitee",
syncGitLabInstance: "URL da instância do GitLab",
syncGitLabInstanceInvalid: "Insira uma URL HTTP ou HTTPS sem credenciais, parâmetros de consulta ou fragmentos.",
+5
View File
@@ -8592,6 +8592,11 @@ export default withEnglishFallback({
sidebarFontSize: "Размер шрифта боковой панели",
sidebarFontSizeDescription: "Размер шрифта дерева объектов боковой панели в пикселях. Не зависит от размера шрифта редактора.",
snippetsDescription: "Настройка шаблонов SQL-сниппетов, вызываемых в редакторе.",
snippetTriggerKey: "Клавиша вызова сниппетов",
snippetTriggerKeyDescription: "Выберите клавишу для вставки сниппетов в редакторе (Tab, пробел или оба).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Пробел",
snippetTriggerKeyBoth: "Tab или пробел",
snippetsPlaceholderHint: "Используйте {'${number:default text}'} для создания редактируемого заполнителя. Заполнители с одинаковым номером остаются связанными; например:",
snippetsAdd: "Добавить сниппет",
snippetsLabel: "Название",
+5
View File
@@ -7281,6 +7281,11 @@ export default withEnglishFallback({
sidebarFontSize: "Kenar çubuğu yazı tipi boyutu",
sidebarFontSizeDescription: "Kenar çubuğu nesne ağacı için piksel cinsinden yazı tipi boyutu. Düzenleyici yazı tipi boyutundan bağımsızdır.",
snippetsDescription: "Düzenleyicide tetiklenen SQL parçacığı şablonlarını özelleştirin.",
snippetTriggerKey: "Kod parçacığı tetikleme tuşu",
snippetTriggerKeyDescription: "Düzenleyicide kod parçacığı genişletmesini tetiklemek için kullanılan tuşu seçin (Tab, Boşluk veya Her İkisi).",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "Boşluk",
snippetTriggerKeyBoth: "Tab veya Boşluk",
snippetsAdd: "Parçacık Ekle",
snippetsLabel: "Etiket",
snippetsPrefix: "Önek",
+5
View File
@@ -8261,6 +8261,11 @@ export default withEnglishFallback({
sidebarFontSize: "侧边栏字体大小",
sidebarFontSizeDescription: "侧边栏对象树的字体大小(像素),独立于编辑器字体大小。",
snippetsDescription: "自定义编辑器中触发的 SQL 代码片段模板。",
snippetTriggerKey: "代码片段触发键",
snippetTriggerKeyDescription: "设置在编辑器中触发插入代码片段的按键(Tab、空格或两者均可)。",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "空格",
snippetTriggerKeyBoth: "Tab 或空格",
snippetsPlaceholderHint: "使用 {'${编号:默认文本}'} 创建可编辑占位符;编号相同的占位符会同步修改,例如:",
snippetsAdd: "添加片段",
snippetsLabel: "显示名",
+5
View File
@@ -6726,6 +6726,11 @@ export default withEnglishFallback({
sidebarShowTooltips: "側邊欄懸浮提示",
sidebarShowTooltipsDescription: "在側邊欄的連線與資料庫物件上懸浮時顯示詳細提示;關閉後懸浮不再彈出。",
snippetsDescription: "自訂編輯器中觸發的 SQL 程式碼片段範本。",
snippetTriggerKey: "程式碼片段觸發鍵",
snippetTriggerKeyDescription: "設定在編輯器中觸發插入程式碼片段的按鍵(Tab、空格或兩者皆可)。",
snippetTriggerKeyTab: "Tab",
snippetTriggerKeySpace: "空格",
snippetTriggerKeyBoth: "Tab 或空格",
syncSnippetProviderGitee: "Gitee 程式碼片段",
syncGitLabInstance: "GitLab 執行個體網址",
syncGitLabInstanceInvalid: "請輸入不含認證資訊、查詢參數或片段的 HTTP 或 HTTPS 網址。",
@@ -91,7 +91,7 @@ function createHarness(options: {
"let cancelPendingCompletionEnter: (() => void) | null = null;",
"let suppressNextSqlCompletionAutoStartUntil = 0;",
"const completion = { get suppressAutoStartUntil() { return suppressNextSqlCompletionAutoStartUntil; }, set suppressAutoStartUntil(value) { suppressNextSqlCompletionAutoStartUntil = value; } };",
"const codeMirrorRuntime = { codeMirrorCompletionStatus, codeMirrorSelectedCompletion, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion, codeMirrorCloseCompletion, codeMirrorInsertNewlineKeepIndent, codeMirrorNextSnippetField, codeMirrorIndentMore };",
"const codeMirrorRuntime = { codeMirrorCompletionStatus, codeMirrorSelectedCompletion, codeMirrorCurrentCompletions: typeof codeMirrorCurrentCompletions !== 'undefined' ? codeMirrorCurrentCompletions : undefined, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion, codeMirrorCloseCompletion, codeMirrorInsertNewlineKeepIndent, codeMirrorNextSnippetField, codeMirrorIndentMore };",
extractFunction("editorIndentUnit"),
extractFunction("handleTab"),
extractFunction("tabKeyAcceptsCompletion"),
@@ -100,8 +100,12 @@ function createHarness(options: {
extractFunction("handleEnter"),
extractFunction("clearPendingCompletionEnter"),
extractFunction("insertNewlineWithoutCompletion"),
extractFunction("getTargetCompletion"),
extractFunction("canAcceptCurrentCompletionOnTab"),
extractFunction("acceptCompletionOrNextSnippetField"),
extractFunction("acceptSnippetCompletionOnSpace"),
extractFunction("acceptSqlServerCompletionOnSpace"),
extractFunction("handleSpace"),
extractFunction("clearPendingCompletionTab"),
extractFunction("waitForCompletionTab"),
extractFunction("consumeSqlCompletionAutoStartSuppression"),
@@ -99,6 +99,12 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("openDataTabsNextToActive");
});
it("includes snippet trigger key setting in draft keys and draft conversion", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("snippetTriggerKey");
const draft = editorSettingsDraftFromSettings(makeSettings({ snippetTriggerKey: "space" }));
expect(draft.snippetTriggerKey).toBe("space");
});
it("includes data grid type colors", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes");
});
@@ -273,12 +273,14 @@ describe("settingsTransfer", () => {
theme: DEFAULT_EDITOR_SETTINGS.theme,
pageSize: 200,
snippets: [{ id: "s1", label: "L", prefix: "p", body: "SELECT 1", enabled: true }],
snippetTriggerKey: "space",
} as EditorSettings;
const result = parseSettingsTransferFile(serializeSettingsTransfer(settings));
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value.editorSettings.pageSize).toBe(200);
expect(result.value.editorSettings.snippets).toEqual(settings.snippets);
expect(result.value.editorSettings.snippetTriggerKey).toBe("space");
});
it("round-trips table default sorting settings", () => {
@@ -30,6 +30,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"tableCompletionSchemaQualification",
"insertSpaceAfterCompletion",
"sqlServerSpaceConfirmsCompletion",
"snippetTriggerKey",
"sortCompletionColumnsAlphabetically",
"selectFirstCompletionOnOpen",
"wordWrap",
@@ -177,6 +178,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
if (key === "csvNullMode") return normalizeCsvNullMode(value);
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
if (key === "snippetTriggerKey") return value === "space" || value === "both" ? value : "tab";
return value;
}
@@ -272,6 +272,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "shortcuts", category: "shortcuts", titleKey: "settings.shortcutsTab", targetId: "shortcuts" },
{ id: "sql-shortcuts", category: "shortcuts", titleKey: "settings.sqlShortcutsTitle", descriptionKey: "settings.sqlShortcutsDescription", targetId: "sql-shortcuts" },
{ id: "snippets", category: "snippets", titleKey: "settings.snippetsTab", descriptionKey: "settings.snippetsDescription", targetId: "snippets" },
{ id: "snippet-trigger-key", category: "snippets", titleKey: "settings.snippetTriggerKey", descriptionKey: "settings.snippetTriggerKeyDescription", targetId: "snippet-trigger-key" },
{ id: "sync-webdav", category: "sync", titleKey: "settings.syncWebDavTitle", descriptionKey: "settings.syncWebDavDescription", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-endpoint", category: "sync", titleKey: "settings.syncEndpoint", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-username", category: "sync", titleKey: "settings.syncUsername", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
@@ -195,7 +195,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"globalDateTimeImportFormat",
],
shortcuts: ["shortcuts", "sqlShortcuts"],
snippets: ["snippets"],
snippets: ["snippets", "snippetTriggerKey"],
other: ["updateDownloadSource", "updateNotificationsEnabled", "autoDownloadUpdates", "autoUpdateApp", "autoUpdateDrivers", "autoUpdateJdbc", "autoUpdateMcp", "autoUpdatePlugins"],
};
@@ -353,6 +353,14 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ sqlServerSpaceConfirmsCompletion: "yes" as unknown as boolean }).sqlServerSpaceConfirmsCompletion).toBe(false);
});
it("defaults snippetTriggerKey to tab and preserves valid options while falling back on invalid values", () => {
expect(normalizeEditorSettings({}).snippetTriggerKey).toBe("tab");
expect(normalizeEditorSettings({ snippetTriggerKey: "tab" }).snippetTriggerKey).toBe("tab");
expect(normalizeEditorSettings({ snippetTriggerKey: "space" }).snippetTriggerKey).toBe("space");
expect(normalizeEditorSettings({ snippetTriggerKey: "both" }).snippetTriggerKey).toBe("both");
expect(normalizeEditorSettings({ snippetTriggerKey: "enter" as any }).snippetTriggerKey).toBe("tab");
});
it("selects the first completion candidate by default and preserves the opt-out", () => {
expect(normalizeEditorSettings({}).selectFirstCompletionOnOpen).toBe(true);
expect(normalizeEditorSettings({ selectFirstCompletionOnOpen: true }).selectFirstCompletionOnOpen).toBe(true);
+6
View File
@@ -851,7 +851,10 @@ export interface RememberedConnectionDatabase {
dbType: string;
}
export type SnippetTriggerKey = "tab" | "space" | "both";
export interface EditorSettings {
snippetTriggerKey: SnippetTriggerKey;
fontFamily: string;
fontSize: number;
uiFontFamily: string;
@@ -1205,6 +1208,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
tableCompletionSchemaQualification: DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION,
insertSpaceAfterCompletion: true,
sqlServerSpaceConfirmsCompletion: false,
snippetTriggerKey: "tab",
sortCompletionColumnsAlphabetically: true,
selectFirstCompletionOnOpen: true,
wordWrap: false,
@@ -1798,6 +1802,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
tableCompletionSchemaQualification: normalizeSqlTableCompletionSchemaQualification(settings.tableCompletionSchemaQualification),
insertSpaceAfterCompletion: typeof settings.insertSpaceAfterCompletion === "boolean" ? settings.insertSpaceAfterCompletion : DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion,
sqlServerSpaceConfirmsCompletion: typeof settings.sqlServerSpaceConfirmsCompletion === "boolean" ? settings.sqlServerSpaceConfirmsCompletion : DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion,
snippetTriggerKey: settings.snippetTriggerKey === "space" || settings.snippetTriggerKey === "both" ? settings.snippetTriggerKey : DEFAULT_EDITOR_SETTINGS.snippetTriggerKey,
sortCompletionColumnsAlphabetically: typeof settings.sortCompletionColumnsAlphabetically === "boolean" ? settings.sortCompletionColumnsAlphabetically : DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically,
selectFirstCompletionOnOpen: typeof settings.selectFirstCompletionOnOpen === "boolean" ? settings.selectFirstCompletionOnOpen : DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
@@ -2645,6 +2650,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.tableCompletionSchemaQualification !== undefined) editorSettings.value.tableCompletionSchemaQualification = normalizeSqlTableCompletionSchemaQualification(partial.tableCompletionSchemaQualification);
if (partial.insertSpaceAfterCompletion !== undefined) editorSettings.value.insertSpaceAfterCompletion = partial.insertSpaceAfterCompletion === true;
if (partial.sqlServerSpaceConfirmsCompletion !== undefined) editorSettings.value.sqlServerSpaceConfirmsCompletion = partial.sqlServerSpaceConfirmsCompletion === true;
if (partial.snippetTriggerKey !== undefined) editorSettings.value.snippetTriggerKey = partial.snippetTriggerKey === "space" || partial.snippetTriggerKey === "both" ? partial.snippetTriggerKey : "tab";
if (partial.sortCompletionColumnsAlphabetically !== undefined) editorSettings.value.sortCompletionColumnsAlphabetically = partial.sortCompletionColumnsAlphabetically === true;
if (partial.selectFirstCompletionOnOpen !== undefined) editorSettings.value.selectFirstCompletionOnOpen = partial.selectFirstCompletionOnOpen === true;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;