feat(editor): execute custom sql template by shortcut on selection

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-08-28 14:27:55 +08:00
1 parent 60bfb41f13
commit 802019daa3
26 files changed
+878 -7

No files matched your search

@@ -154,8 +154,9 @@ import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COL
import { isMacOS, isWindows } from "@/lib/backend/platform";
import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState";
import { useToast } from "@/composables/useToast";
import type { DatabaseType, SqlSnippet } from "@/types/database";
import type { DatabaseType, SqlShortcutAction, SqlSnippet } from "@/types/database";
import { uuid } from "@/lib/common/utils";
import { findSqlShortcutConflicts, hasSqlShortcutConflicts as sqlShortcutsHaveConflicts, SQL_SHORTCUT_TABLE_TOKEN } from "@/lib/sql/sqlShortcutActions";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
import AppLogo from "@/components/icons/AppLogo.vue";
@@ -563,6 +564,12 @@ function editableSnippet(snippet: SqlSnippet): SqlSnippet {
const editSnippets = ref<SqlSnippet[]>(settingsStore.editorSettings.snippets.map(editableSnippet));
function editableSqlShortcut(action: SqlShortcutAction): SqlShortcutAction {
return { ...action, enabled: action.enabled !== false };
}
const editSqlShortcuts = ref<SqlShortcutAction[]>(settingsStore.editorSettings.sqlShortcuts.map(editableSqlShortcut));
function currentEditorSettingsDraft(): EditorSettingsDraft {
return {
fontFamily: editFontFamily.value,
@@ -652,6 +659,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
updateDownloadSource: editUpdateDownloadSource.value,
toolbarItems: { ...editToolbarItems.value },
snippets: editSnippets.value,
sqlShortcuts: editSqlShortcuts.value,
sqlVariableSubstitutionEnabled: editSqlVariableSubstitutionEnabled.value,
sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides.value,
clickTableNavigationTarget: editClickTableNavigationTarget.value,
@@ -665,6 +673,12 @@ const snippetDialogOpen = ref(false);
const snippetEditingId = ref<string | null>(null);
const snippetForm = ref({ label: "", prefix: "", body: "" });
const snippetFormPrefixError = ref("");
const sqlShortcutDialogOpen = ref(false);
const sqlShortcutEditingId = ref<string | null>(null);
const sqlShortcutForm = ref({ label: "", shortcut: "", sql: "" });
const sqlShortcutFormLabelError = ref("");
const editingSqlShortcutInputId = ref<string | null>(null);
const iconThemeBlackDescriptionText = computed(() => (isMacOS() ? t("settings.iconThemeBlackDescriptionMac") : t("settings.iconThemeBlackDescription")));
const layoutDescTruncated = {
separated: ref<boolean>(false),
@@ -792,6 +806,127 @@ function confirmDeleteSnippet(snippet: SqlSnippet) {
}
}
function openAddSqlShortcutDialog() {
sqlShortcutEditingId.value = null;
sqlShortcutForm.value = { label: "", shortcut: "", sql: `SELECT * FROM ${SQL_SHORTCUT_TABLE_TOKEN}` };
sqlShortcutFormLabelError.value = "";
editingSqlShortcutInputId.value = null;
sqlShortcutDialogOpen.value = true;
}
function openEditSqlShortcutDialog(action: SqlShortcutAction) {
sqlShortcutEditingId.value = action.id;
sqlShortcutForm.value = {
label: action.label,
shortcut: action.shortcut,
sql: action.sql,
};
sqlShortcutFormLabelError.value = "";
editingSqlShortcutInputId.value = null;
sqlShortcutDialogOpen.value = true;
}
function saveSqlShortcut() {
const label = sqlShortcutForm.value.label.trim();
if (!label) {
sqlShortcutFormLabelError.value = t("settings.sqlShortcutsLabelRequired");
return;
}
const payload: SqlShortcutAction = {
id: sqlShortcutEditingId.value ?? uuid(),
label,
shortcut: sqlShortcutForm.value.shortcut.trim(),
sql: sqlShortcutForm.value.sql,
enabled: true,
};
const nextShortcuts = sqlShortcutEditingId.value
? editSqlShortcuts.value.map((item) =>
item.id === sqlShortcutEditingId.value
? {
...payload,
enabled: item.enabled !== false,
}
: item,
)
: [...editSqlShortcuts.value, payload];
if (sqlShortcutsHaveConflicts(nextShortcuts, editShortcuts.value)) {
toast(t("settings.shortcutConflict"), 3000);
return;
}
editSqlShortcuts.value = nextShortcuts;
sqlShortcutDialogOpen.value = false;
void commitSqlShortcuts();
}
function setSqlShortcutEnabled(id: string, enabled: boolean) {
const idx = editSqlShortcuts.value.findIndex((item) => item.id === id);
if (idx === -1) return;
if (enabled) {
const next = editSqlShortcuts.value.map((item) => (item.id === id ? { ...item, enabled } : item));
if (sqlShortcutsHaveConflicts(next, editShortcuts.value)) {
toast(t("settings.shortcutConflict"), 3000);
return;
}
}
editSqlShortcuts.value[idx] = { ...editSqlShortcuts.value[idx], enabled };
void commitSqlShortcuts();
}
function deleteSqlShortcut(id: string) {
editSqlShortcuts.value = editSqlShortcuts.value.filter((item) => item.id !== id);
void commitSqlShortcuts();
}
function confirmDeleteSqlShortcut(action: SqlShortcutAction) {
if (window.confirm(t("settings.sqlShortcutsDeleteConfirm", { name: action.label }))) {
deleteSqlShortcut(action.id);
}
}
function onSqlShortcutBindingKeydown(id: string, event: KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
if (editingSqlShortcutInputId.value !== id) return;
if (event.key === "Escape") {
editingSqlShortcutInputId.value = null;
return;
}
const shortcut = eventToShortcut(event);
if (!shortcut) return;
sqlShortcutForm.value = { ...sqlShortcutForm.value, shortcut };
editingSqlShortcutInputId.value = null;
}
function focusSqlShortcutInput(id: string) {
editingSqlShortcutInputId.value = id;
const input = document.querySelector<HTMLInputElement>(`[data-sql-shortcut-input="${id}"]`);
requestAnimationFrame(() => {
input?.focus();
input?.select();
});
}
function cancelSqlShortcutInputEdit() {
editingSqlShortcutInputId.value = null;
}
function clearSqlShortcutBinding() {
sqlShortcutForm.value = { ...sqlShortcutForm.value, shortcut: "" };
}
async function commitSqlShortcuts() {
const sqlShortcuts = editSqlShortcuts.value.map(editableSqlShortcut);
try {
await settingsStore.updateEditorSettingsAndPersist({ sqlShortcuts });
editEditorSettingsBase.value = {
...editEditorSettingsBase.value,
sqlShortcuts: JSON.parse(JSON.stringify(sqlShortcuts)) as SqlShortcutAction[],
};
} catch (error) {
applySettingsErrorToast(error);
}
}
const uiFontPreviewValues = new Set([DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY]);
const systemFontOptions = computed(() => {
@@ -920,6 +1055,7 @@ function syncEditorSettingsDraftFromStore() {
editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource;
editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems };
editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet);
editSqlShortcuts.value = settingsStore.editorSettings.sqlShortcuts.map(editableSqlShortcut);
editSqlVariableSubstitutionEnabled.value = settingsStore.editorSettings.sqlVariableSubstitutionEnabled;
editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides);
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
@@ -960,6 +1096,8 @@ const shortcutConflicts = computed(() =>
return conflict ? [definition.id] : [];
}),
);
const sqlShortcutConflicts = computed(() => findSqlShortcutConflicts(editSqlShortcuts.value, editShortcuts.value));
const hasSqlShortcutConflicts = computed(() => sqlShortcutConflicts.value.length > 0);
const shortcutSearchQuery = ref("");
const formatterEditorShortcutIds: ShortcutActionId[] = [
"formatSql",
@@ -997,8 +1135,13 @@ const filteredShortcutDefinitions = computed(() => {
});
const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0);
const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.shortcuts));
const sqlShortcutsChanged = computed(() => JSON.stringify(editSqlShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.sqlShortcuts));
const duckDbWorkerSettingsRequireRestart = computed(() => editDuckDbWorkerProcessIsolation.value !== startupDuckDbWorkerProcessIsolation.value || normalizeDuckDbWorkerMaxProcesses(editDuckDbWorkerMaxProcesses.value) !== startupDuckDbWorkerMaxProcesses.value);
const hasBlockingShortcutConflicts = computed(() => shortcutsChanged.value && hasShortcutConflicts.value);
const hasBlockingShortcutConflicts = computed(() => {
const shortcutDraftTouched = shortcutsChanged.value || sqlShortcutsChanged.value;
if (!shortcutDraftTouched) return false;
return hasShortcutConflicts.value || hasSqlShortcutConflicts.value;
});
const hasBlockingFormatterConfig = computed(() => activeSettingsTab.value === "formatter" && !sqlFormatterConfigValid.value);
const hasBlockingQueryResultRowLimit = computed(() => editQueryResultMaxRowsEnabled.value && editQueryResultMaxRows.value < editPageSize.value);
const hasApplyBlocker = computed(() => hasBlockingShortcutConflicts.value || hasBlockingFormatterConfig.value || hasBlockingQueryResultRowLimit.value);
@@ -1304,6 +1447,7 @@ function resetAllDefaults() {
editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
editSqlShortcuts.value = [];
}
function addTableColumnTemplateRow() {
@@ -5957,6 +6101,70 @@ onUnmounted(() => {
</div>
</div>
</div>
<div class="mt-6 border-t border-border/70 pt-6" data-settings-search-id="sql-shortcuts">
<div class="mb-4 flex items-center justify-between gap-3">
<div class="min-w-0">
<h3 class="text-sm font-medium">{{ t("settings.sqlShortcutsTitle") }}</h3>
<p class="mt-1 text-sm text-muted-foreground">
{{ t("settings.sqlShortcutsDescription") }}
</p>
</div>
<Button variant="outline" size="sm" class="shrink-0" @click="openAddSqlShortcutDialog">
<Plus class="mr-2 h-4 w-4" />
{{ t("settings.sqlShortcutsAdd") }}
</Button>
</div>
<div v-if="editSqlShortcuts.length === 0" class="rounded-md border border-dashed border-border/70 px-3 py-8 text-center text-sm text-muted-foreground">
{{ t("settings.sqlShortcutsEmpty") }}
</div>
<div v-else class="overflow-x-auto rounded-md border">
<table class="w-full min-w-[720px] text-sm">
<thead>
<tr class="border-b bg-muted/50">
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">{{ t("settings.sqlShortcutsLabel") }}</th>
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">{{ t("settings.shortcutPressShortcut") }}</th>
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">{{ t("settings.snippetsStatus") }}</th>
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">{{ t("settings.sqlShortcutsSql") }}</th>
<th class="px-3 py-2 w-20"></th>
</tr>
</thead>
<tbody>
<tr v-for="action in editSqlShortcuts" :key="action.id" class="border-b last:border-b-0 hover:bg-muted/30" :class="action.enabled === false ? 'text-muted-foreground' : ''">
<td class="px-3 py-2">{{ action.label }}</td>
<td class="px-3 py-2">
<Badge variant="outline" class="h-5 rounded-md px-1.5 font-mono text-[11px] text-muted-foreground">
{{ action.shortcut ? formatShortcutPill(action.shortcut) : t("settings.sqlShortcutsUnbound") }}
</Badge>
</td>
<td class="px-3 py-2">
<div class="flex items-center gap-2">
<Switch :id="`sql-shortcut-enabled-${action.id}`" :model-value="action.enabled !== false" size="sm" :aria-label="t('settings.sqlShortcutsToggle')" @update:model-value="(value: boolean) => setSqlShortcutEnabled(action.id, value)" />
<Label :for="`sql-shortcut-enabled-${action.id}`" class="text-xs font-normal text-muted-foreground">
{{ action.enabled === false ? t("settings.snippetsDisabled") : t("settings.snippetsEnabled") }}
</Label>
</div>
</td>
<td class="max-w-[300px] truncate px-3 py-2 font-mono text-xs text-muted-foreground">{{ action.sql }}</td>
<td class="px-3 py-2">
<div class="flex items-center gap-1">
<Button variant="ghost" size="icon-xs" @click="openEditSqlShortcutDialog(action)">
<Pencil class="size-3.5" />
</Button>
<Button variant="ghost" size="icon-xs" @click="confirmDeleteSqlShortcut(action)">
<Trash2 class="size-3.5" />
</Button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<p v-if="hasSqlShortcutConflicts" class="mt-2 text-xs text-destructive">
{{ t("settings.shortcutConflict") }}
</p>
</div>
</section>
<!-- Snippets Tab -->
@@ -7527,6 +7735,68 @@ onUnmounted(() => {
</DialogContent>
</Dialog>
<!-- SQL Shortcut Add/Edit Dialog -->
<Dialog :open="sqlShortcutDialogOpen" @update:open="sqlShortcutDialogOpen = $event">
<DialogContent class="sm:max-w-[560px]">
<DialogHeader>
<DialogTitle>
{{ sqlShortcutEditingId ? t("settings.sqlShortcutsEditTitle") : t("settings.sqlShortcutsAddTitle") }}
</DialogTitle>
</DialogHeader>
<div class="flex flex-col gap-4 py-2">
<div class="flex flex-col gap-1.5">
<Label for="sql-shortcut-label">{{ t("settings.sqlShortcutsLabel") }}</Label>
<Input id="sql-shortcut-label" v-model="sqlShortcutForm.label" :placeholder="t('settings.sqlShortcutsLabelPlaceholder')" />
<p v-if="sqlShortcutFormLabelError" class="text-xs text-destructive">
{{ sqlShortcutFormLabelError }}
</p>
</div>
<div class="flex flex-col gap-1.5">
<Label for="sql-shortcut-binding">{{ t("settings.shortcutPressShortcut") }}</Label>
<div class="flex items-center gap-2">
<input
id="sql-shortcut-binding"
data-sql-shortcut-input="dialog"
:value="editingSqlShortcutInputId === 'dialog' ? '' : formatShortcutPill(sqlShortcutForm.shortcut)"
:style="{ width: editingSqlShortcutInputId === 'dialog' ? shortcutPressShortcutInputWidth : `${Math.max(4, formatShortcutPill(sqlShortcutForm.shortcut).length + 3)}ch` }"
readonly
:placeholder="t('settings.shortcutPressShortcut')"
class="h-8 w-auto min-w-12 max-w-64 shrink-0 cursor-default rounded-[6px] border border-input bg-muted px-2.5 text-center font-mono text-[13px] font-semibold text-foreground/75 shadow-inner outline-none selection:bg-transparent placeholder:text-muted-foreground"
:class="editingSqlShortcutInputId === 'dialog' ? 'max-w-64 cursor-text border-border/80 bg-background text-left text-foreground shadow-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/35' : ''"
@keydown="(event: KeyboardEvent) => onSqlShortcutBindingKeydown('dialog', event)"
/>
<Button v-if="editingSqlShortcutInputId !== 'dialog'" type="button" variant="ghost" size="icon" class="h-8 w-8 shrink-0" :aria-label="t('settings.shortcutPressShortcut')" @click="focusSqlShortcutInput('dialog')">
<Pencil class="h-4 w-4" />
</Button>
<Button v-else type="button" variant="ghost" size="sm" class="h-8 shrink-0 px-2 text-sm font-medium text-muted-foreground hover:text-foreground" @click="cancelSqlShortcutInputEdit">
{{ t("settings.cancel") }}
</Button>
<Button v-if="sqlShortcutForm.shortcut" type="button" variant="ghost" size="icon" class="h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive" :aria-label="t('settings.shortcutClear')" @click="clearSqlShortcutBinding">
<X class="h-4 w-4" />
</Button>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Label for="sql-shortcut-sql">{{ t("settings.sqlShortcutsSql") }}</Label>
<textarea
id="sql-shortcut-sql"
v-model="sqlShortcutForm.sql"
:placeholder="t('settings.sqlShortcutsSqlPlaceholder')"
rows="6"
class="flex min-h-[120px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<p class="text-xs text-muted-foreground">
{{ t("settings.sqlShortcutsVariableHint", { token: SQL_SHORTCUT_TABLE_TOKEN }) }}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="sqlShortcutDialogOpen = false">{{ t("settings.cancel") }}</Button>
<Button @click="saveSqlShortcut">{{ t("settings.save") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- AI Config Delete Confirmation -->
<DangerConfirmDialog v-model:open="aiDeleteConfirmOpen" :title="t('ai.deleteConfigTitle')" :message="t('ai.deleteConfigConfirm')" :confirm-label="t('common.delete')" @confirm="aiConfirmDeleteConfig" />
<DangerConfirmDialog
@@ -112,6 +112,7 @@ import { isPointOverElementRoot } from "@/lib/editor/tableReferenceDragFeedback"
import type { SqlHighlighter } from "@/lib/sql/sqlHighlighter";
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, loadEditorTheme, editorFontTheme, shellLineCommentTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes";
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
import { createQueryEditorSqlShortcutDomHandler, isCharacterProducingShortcut } from "@/lib/editor/queryEditorSqlShortcut";
import { createQueryEditorReplaceShortcutBindings, createQueryEditorReplaceShortcutHandler, createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers";
import { searchKeymapWithoutModD } from "@/lib/editor/codemirrorSearchKeymap";
@@ -119,6 +120,7 @@ import { defaultKeymapForGlobalShortcuts } from "@/lib/editor/codemirrorDefaultK
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
import { clampEditorFontSize, createEditorWheelZoomGestureGuard, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
import { enabledSqlShortcutActions, resolveSqlShortcutTemplate } from "@/lib/sql/sqlShortcutActions";
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer";
@@ -1927,6 +1929,18 @@ function handleSqlIntentionActions(currentView: EditorViewType): boolean {
}
}
function runSqlShortcutAction(action: ReturnType<typeof enabledSqlShortcutActions>[number], currentView: EditorViewType, event?: KeyboardEvent): boolean {
if (shouldBlockExecutionShortcut(event, currentView)) return true;
if (props.readOnly) return true;
const { from, to, empty } = currentView.state.selection.main;
if (empty) return false;
const selected = currentView.state.sliceDoc(from, to).trim();
if (!selected) return false;
const sql = resolveSqlShortcutTemplate(action.sql, selected);
emitExecutionRequest(sql);
return true;
}
function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view"))["keymap"]) {
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
const Prec = codeMirrorPrec;
@@ -1947,11 +1961,22 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
openReplace,
isReadOnly: () => !!props.readOnly,
});
const sqlShortcutActions = enabledSqlShortcutActions(settingsStore.editorSettings.sqlShortcuts);
const sqlShortcutKeymapActions = sqlShortcutActions.filter((action) => !isCharacterProducingShortcut(action.shortcut));
const sqlShortcutBindings = sqlShortcutKeymapActions.flatMap((action) => binding(action.shortcut, (currentView) => runSqlShortcutAction(action, currentView)));
const sqlShortcutDomHandler = createQueryEditorSqlShortcutDomHandler(
() => settingsStore.editorSettings.sqlShortcuts,
(action, currentView, event) => runSqlShortcutAction(action, currentView, event),
);
const combinedDomKeydownHandler = (event: KeyboardEvent, view: EditorViewType) => {
if (replaceShortcutHandler(event)) return true;
return sqlShortcutDomHandler(event, view);
};
return [
editorViewModule
? (Prec?.high(
editorViewModule.EditorView.domEventHandlers({
keydown: replaceShortcutHandler,
keydown: combinedDomKeydownHandler,
}),
) ?? [])
: [],
@@ -2018,6 +2043,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
openReplace,
isReadOnly: () => !!props.readOnly,
}),
...sqlShortcutBindings,
]),
) ?? [],
codeMirrorKeymap.of(
@@ -5967,10 +5993,19 @@ watch(
);
watch(
() => settingsStore.editorSettings.shortcuts,
() => [settingsStore.editorSettings.shortcuts, settingsStore.editorSettings.sqlShortcuts],
() => {
if (!view.value || !defaultKeymapComp) return;
view.value.dispatch({ effects: defaultKeymapComp.reconfigure(defaultKeymapExtension()) });
if (!view.value || !editorViewModule) return;
const effects = [];
if (defaultKeymapComp) {
effects.push(defaultKeymapComp.reconfigure(defaultKeymapExtension()));
}
if (runKeymapComp) {
effects.push(runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap)));
}
if (effects.length > 0) {
view.value.dispatch({ effects });
}
},
{ deep: true },
);