feat: add query editor context menu
This commit is contained in:
parent
3ba3a644fe
commit
bd78436ab1
|
|
@ -5,11 +5,20 @@ import type { CompletionContext } from "@codemirror/autocomplete";
|
|||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { search as cmSearch } from "@codemirror/search";
|
||||
import EditorSearchPanel from "./EditorSearchPanel.vue";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import {
|
||||
buildSqlCompletionItemsFromContext,
|
||||
getSqlFunctionSignatureHelp,
|
||||
|
|
@ -76,6 +85,7 @@ const connectionStore = useConnectionStore();
|
|||
const settingsStore = useSettingsStore();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
||||
const SQL_FUNCTION_NAMES = [
|
||||
"COUNT",
|
||||
|
|
@ -133,6 +143,15 @@ const gestureStartFontSize = ref(settingsStore.editorSettings.fontSize);
|
|||
const isGestureZooming = ref(false);
|
||||
|
||||
const searchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const selectedSql = ref("");
|
||||
const executableSql = ref("");
|
||||
|
||||
const hasSelectedSql = computed(() => selectedSql.value.trim().length > 0);
|
||||
const canCopySelectedSql = computed(() => selectedSql.value.length > 0);
|
||||
const canExecuteContextSql = computed(() => executableSql.value.trim().length > 0);
|
||||
const executeContextMenuLabel = computed(() =>
|
||||
t(hasSelectedSql.value ? "editor.contextMenu.executeSelection" : "editor.contextMenu.executeCurrent"),
|
||||
);
|
||||
|
||||
interface EditorGestureEvent extends Event {
|
||||
scale?: number;
|
||||
|
|
@ -279,6 +298,42 @@ function executeCurrentSql() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function syncContextMenuState(currentView: EditorViewType) {
|
||||
selectedSql.value = selectedSqlFromView(currentView);
|
||||
executableSql.value = executableSqlFromView(currentView);
|
||||
}
|
||||
|
||||
function focusEditor() {
|
||||
view.value?.focus();
|
||||
}
|
||||
|
||||
function executeFromContextMenu() {
|
||||
if (!canExecuteContextSql.value) return;
|
||||
executeCurrentSql();
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
async function copySelectedSqlFromContextMenu() {
|
||||
if (!canCopySelectedSql.value) return;
|
||||
try {
|
||||
await copyToClipboard(selectedSql.value);
|
||||
toast(t("grid.copied"));
|
||||
focusEditor();
|
||||
} catch (e: any) {
|
||||
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllSqlFromContextMenu() {
|
||||
const currentView = view.value;
|
||||
if (!currentView) return;
|
||||
currentView.dispatch({
|
||||
selection: { anchor: 0, head: currentView.state.doc.length },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view"))["keymap"]) {
|
||||
const shortcuts = settingsStore.editorSettings.shortcuts;
|
||||
const Prec = codeMirrorPrec;
|
||||
|
|
@ -1182,6 +1237,7 @@ onMounted(async () => {
|
|||
}
|
||||
}
|
||||
if (update.selectionSet || update.docChanged) {
|
||||
syncContextMenuState(update.view);
|
||||
emit("selectionChange", selectedSqlFromView(update.view));
|
||||
emit("cursorChange", update.state.selection.main.head);
|
||||
}
|
||||
|
|
@ -1336,6 +1392,7 @@ onMounted(async () => {
|
|||
});
|
||||
|
||||
view.value = new EditorView({ state, parent: editorRef.value });
|
||||
syncContextMenuState(view.value);
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
|
||||
cachedTables = [];
|
||||
|
|
@ -1457,7 +1514,23 @@ defineExpose({ openSearch, openReplace });
|
|||
@gesturechange="onEditorGestureChange"
|
||||
@gestureend="onEditorGestureEnd"
|
||||
>
|
||||
<div ref="editorRef" data-query-editor-root class="h-full w-full overflow-hidden" />
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger as-child>
|
||||
<div ref="editorRef" data-query-editor-root data-context-menu class="h-full w-full overflow-hidden" />
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent class="w-52">
|
||||
<ContextMenuItem :disabled="!canExecuteContextSql" @click="executeFromContextMenu">
|
||||
{{ executeContextMenuLabel }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem :disabled="!canCopySelectedSql" @click="copySelectedSqlFromContextMenu">
|
||||
{{ t("editor.contextMenu.copySelection") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="selectAllSqlFromContextMenu">
|
||||
{{ t("editor.contextMenu.selectAll") }}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<EditorSearchPanel ref="searchPanelRef" :view="view" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -310,6 +310,12 @@ export default {
|
|||
JSON_ARRAY: "Creates a JSON array",
|
||||
},
|
||||
},
|
||||
contextMenu: {
|
||||
executeSelection: "Execute selection",
|
||||
executeCurrent: "Execute SQL",
|
||||
copySelection: "Copy selection",
|
||||
selectAll: "Select all",
|
||||
},
|
||||
search: {
|
||||
find: "Find",
|
||||
replace: "Replace",
|
||||
|
|
|
|||
|
|
@ -303,6 +303,12 @@ export default {
|
|||
JSON_ARRAY: "Crea un array JSON",
|
||||
},
|
||||
},
|
||||
contextMenu: {
|
||||
executeSelection: "Ejecutar seleccion",
|
||||
executeCurrent: "Ejecutar SQL",
|
||||
copySelection: "Copiar seleccion",
|
||||
selectAll: "Seleccionar todo",
|
||||
},
|
||||
search: {
|
||||
find: "Buscar",
|
||||
replace: "Reemplazar",
|
||||
|
|
|
|||
|
|
@ -306,6 +306,12 @@ export default {
|
|||
JSON_ARRAY: "创建 JSON 数组",
|
||||
},
|
||||
},
|
||||
contextMenu: {
|
||||
executeSelection: "执行选中 SQL",
|
||||
executeCurrent: "执行 SQL",
|
||||
copySelection: "复制选中内容",
|
||||
selectAll: "全选",
|
||||
},
|
||||
search: {
|
||||
find: "查找",
|
||||
replace: "替换",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ test("query editor localizes the replace all button", () => {
|
|||
assert.doesNotMatch(searchPanelSource, />\s*全部\s*</);
|
||||
});
|
||||
|
||||
test("query editor exposes a context menu for executing selected SQL", () => {
|
||||
assert.match(source, /ContextMenuContent/);
|
||||
assert.match(source, /data-context-menu/);
|
||||
assert.match(source, /syncContextMenuState\(update\.view\)/);
|
||||
assert.match(source, /executeSelection/);
|
||||
assert.match(source, /copySelection/);
|
||||
assert.match(source, /selectAllSqlFromContextMenu/);
|
||||
assert.match(appSource, /\[data-context-menu\]/);
|
||||
});
|
||||
|
||||
test("query editor does not apply custom search match highlight styles", () => {
|
||||
assert.doesNotMatch(editorThemeSource, /"\.cm-searchMatch"/);
|
||||
assert.doesNotMatch(editorThemeSource, /"\.cm-searchMatch-selected"/);
|
||||
|
|
|
|||
Loading…
Reference in New Issue