mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
feat(editor): support customizing snippet trigger key
This commit is contained in:
@@ -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({
|
||||
|
||||
Vendored
+6
@@ -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}"`);
|
||||
});
|
||||
});
|
||||
|
||||
+66
-1
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 を入力してください。",
|
||||
|
||||
@@ -7178,6 +7178,11 @@ export default withEnglishFallback({
|
||||
sidebarShowTooltips: "사이드바 호버 툴팁",
|
||||
sidebarShowTooltipsDescription: "사이드바에서 연결 및 데이터베이스 개체에 마우스를 올리면 상세 툴팁을 표시합니다. 끄면 표시되지 않습니다.",
|
||||
snippetsDescription: "편집기에서 트리거되는 SQL 스니펫 템플릿을 사용자 정의합니다.",
|
||||
snippetTriggerKey: "스니펫 트리거 키",
|
||||
snippetTriggerKeyDescription: "편집기에서 스니펫 확장을 트리거하는 키를 설정합니다(Tab, 스페이스 또는 둘 다).",
|
||||
snippetTriggerKeyTab: "Tab",
|
||||
snippetTriggerKeySpace: "스페이스",
|
||||
snippetTriggerKeyBoth: "Tab 또는 스페이스",
|
||||
snippetsPlaceholderHint: "{'${번호:기본 텍스트}'}를 사용하여 편집 가능한 자리 표시자를 만듭니다. 같은 번호의 자리 표시자는 함께 수정됩니다. 예:",
|
||||
snippetsAdd: "스니펫 추가",
|
||||
snippetsLabel: "라벨",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: "Название",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8261,6 +8261,11 @@ export default withEnglishFallback({
|
||||
sidebarFontSize: "侧边栏字体大小",
|
||||
sidebarFontSizeDescription: "侧边栏对象树的字体大小(像素),独立于编辑器字体大小。",
|
||||
snippetsDescription: "自定义编辑器中触发的 SQL 代码片段模板。",
|
||||
snippetTriggerKey: "代码片段触发键",
|
||||
snippetTriggerKeyDescription: "设置在编辑器中触发插入代码片段的按键(Tab、空格或两者均可)。",
|
||||
snippetTriggerKeyTab: "Tab",
|
||||
snippetTriggerKeySpace: "空格",
|
||||
snippetTriggerKeyBoth: "Tab 或空格",
|
||||
snippetsPlaceholderHint: "使用 {'${编号:默认文本}'} 创建可编辑占位符;编号相同的占位符会同步修改,例如:",
|
||||
snippetsAdd: "添加片段",
|
||||
snippetsLabel: "显示名",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user