test(editor): cover context menu routing payloads
This commit is contained in:
1 parent
d3118256a4
commit
711d0ec57b
13 files changed
+484
-197
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
|
||||
import { CaseLower, CaseUpper, FileCode, PencilRuler, Play, Copy, Sparkles, Table2, TextSelect } from "@lucide/vue";
|
||||
import { CaseLower, CaseUpper, Code2, FileCode, Pencil, PencilRuler, Play, Copy, Sparkles, Table2, TextSelect } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
@@ -40,7 +40,7 @@ import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget }
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
|
||||
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
|
||||
import { resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { extractIdentifierDetailsAt, isSqlKeyword, matchTable, splitQualifiedIdentifier, sqlObjectHoverDetail, sqlObjectNavigationTarget, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
|
||||
import { extractIdentifierDetailsAt, isSqlKeyword, matchTable, mergeSqlObjectNavigationType, splitQualifiedIdentifier, sqlObjectHoverDetail, sqlObjectNavigationTarget, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
|
||||
import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sql/sqlDiagnostics";
|
||||
import {
|
||||
DBX_TABLE_REFERENCE_MIME,
|
||||
@@ -65,7 +65,7 @@ import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorP
|
||||
import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation";
|
||||
import { isSchemaAware, isSingleDatabase, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
|
||||
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy";
|
||||
import { qualifiedTableNameAtSqlPosition } from "@/lib/sql/queryCursorTableTarget";
|
||||
import { queryContextObjectActions, queryContextObjectRoute, queryTableCandidateAtSqlPosition, resolveQueryContextCandidateDatabase, resolveQueryContextObjectTarget, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { areSqlSemanticDiagnosticsEqual, buildSqlParserErrorDiagnostic, buildSqlSemanticDiagnostics, isSqlSemanticDiagnosticInputContext, shouldRunSqlSemanticDiagnostics, sqlSemanticDiagnosticRangesForViewport, tableReferenceKey, type SqlSemanticDiagnostic } from "@/lib/sql/semantic/diagnostics";
|
||||
import { buildRedisSyntaxDiagnostics, shouldRunRedisDiagnostics } from "@/lib/redis/redisSyntaxDiagnostics";
|
||||
@@ -105,9 +105,10 @@ const emit = defineEmits<{
|
||||
execute: [source: SqlExecutionOverride];
|
||||
save: [];
|
||||
clickTable: [target: SqlObjectNavigationTarget];
|
||||
viewTableData: [tableName: string];
|
||||
viewTableDdl: [tableName: string];
|
||||
editTableStructure: [tableName: string];
|
||||
viewTableData: [target: SqlObjectNavigationTarget];
|
||||
viewTableDdl: [target: SqlObjectNavigationTarget];
|
||||
editTableStructure: [target: SqlObjectNavigationTarget];
|
||||
openObjectSource: [target: SqlObjectNavigationTarget, initialEditing: boolean];
|
||||
clickColumn: [columns: Array<{ name: string; table: string; schema?: string }>, error?: string | undefined];
|
||||
closeColumnPanel: [];
|
||||
viewportChange: [viewport: { scrollTop: number; scrollLeft: number }];
|
||||
@@ -188,7 +189,7 @@ const isGestureZooming = ref(false);
|
||||
const searchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const selectedSql = ref("");
|
||||
const executableSql = ref("");
|
||||
const contextTableName = ref<string | null>(null);
|
||||
const contextObjectTarget = ref<SqlObjectNavigationTarget | null>(null);
|
||||
|
||||
const hasSelectedSql = computed(() => selectedSql.value.trim().length > 0);
|
||||
const canCopySelectedSql = computed(() => selectedSql.value.length > 0);
|
||||
@@ -285,7 +286,7 @@ function editorThemeAppearance() {
|
||||
}
|
||||
|
||||
// Completion cache
|
||||
let cachedTables: Array<{ name: string; schema?: string; type?: "table" | "view" }> = [];
|
||||
let cachedTables: SqlCompletionTable[] = [];
|
||||
let cachedCompletionObjects: SqlCompletionObject[] = [];
|
||||
// Persistent column cache keyed by "schema.table" or "table"
|
||||
const cachedColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
@@ -554,7 +555,28 @@ function syncContextMenuState(currentView: EditorViewType) {
|
||||
function syncContextMenuStateAtEvent(currentView: EditorViewType, event: MouseEvent) {
|
||||
syncContextMenuState(currentView);
|
||||
const pos = currentView.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||
contextTableName.value = pos == null ? null : qualifiedTableNameAtSqlPosition(currentView.state.doc.toString(), pos);
|
||||
if (pos == null) {
|
||||
contextObjectTarget.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = currentView.state.doc.toString();
|
||||
if (!props.connectionId || props.database == null) {
|
||||
const candidate = queryTableCandidateAtSqlPosition({ connectionId: "", database: props.database ?? "", schema: props.schema, databaseType: props.databaseType, sql, position: pos });
|
||||
contextObjectTarget.value = candidate ? { name: candidate.tableName, database: candidate.database, schema: candidate.schema } : null;
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedCandidate = queryTableCandidateAtSqlPosition({ connectionId: props.connectionId, database: props.database, schema: props.schema, databaseType: props.databaseType, sql, position: pos });
|
||||
if (!parsedCandidate) {
|
||||
contextObjectTarget.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Right-click must stay instant: resolve from completion/tree caches and keep the legacy table fallback when metadata is unavailable.
|
||||
const candidate = resolveQueryContextCandidateDatabase(parsedCandidate, connectionStore.lookupLocalCompletionDatabases(parsedCandidate.connectionId, parsedCandidate.database, MAX_COMPLETION_TABLES));
|
||||
const tables = connectionStore.lookupLocalCompletionTables(candidate.connectionId, candidate.database, candidate.tableName, MAX_COMPLETION_TABLES, candidate.schema);
|
||||
contextObjectTarget.value = resolveQueryContextObjectTarget(candidate, tables);
|
||||
}
|
||||
|
||||
function focusEditor() {
|
||||
@@ -849,22 +871,40 @@ async function pasteClipboardAsSqlInCondition(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function openTableFromContextMenu() {
|
||||
if (!contextTableName.value) return;
|
||||
emit("viewTableData", contextTableName.value);
|
||||
function emitContextObjectAction(action: QueryContextObjectAction) {
|
||||
if (!contextObjectTarget.value) return;
|
||||
const route = queryContextObjectRoute(action, contextObjectTarget.value);
|
||||
switch (route.event) {
|
||||
case "viewTableData":
|
||||
emit("viewTableData", route.payload[0]);
|
||||
break;
|
||||
case "editTableStructure":
|
||||
emit("editTableStructure", route.payload[0]);
|
||||
break;
|
||||
case "openObjectSource":
|
||||
emit("openObjectSource", route.payload[0], route.payload[1]);
|
||||
break;
|
||||
case "viewTableDdl":
|
||||
emit("viewTableDdl", route.payload[0]);
|
||||
break;
|
||||
}
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function editTableStructureFromContextMenu() {
|
||||
if (!contextTableName.value) return;
|
||||
emit("editTableStructure", contextTableName.value);
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function openTableDdlFromContextMenu() {
|
||||
if (!contextTableName.value) return;
|
||||
emit("viewTableDdl", contextTableName.value);
|
||||
focusEditor();
|
||||
function contextObjectMenuItem(action: QueryContextObjectAction): ContextMenuItem {
|
||||
const disabled = !contextObjectTarget.value;
|
||||
switch (action) {
|
||||
case "view-data":
|
||||
return { label: t("contextMenu.viewData"), action: () => emitContextObjectAction(action), disabled, icon: Table2 };
|
||||
case "edit-table-structure":
|
||||
return { label: t("contextMenu.editStructure"), action: () => emitContextObjectAction(action), disabled, icon: PencilRuler };
|
||||
case "edit-view":
|
||||
return { label: t("contextMenu.editView"), action: () => emitContextObjectAction(action), disabled, icon: Pencil };
|
||||
case "view-source":
|
||||
return { label: t("contextMenu.viewSource"), action: () => emitContextObjectAction(action), disabled, icon: Code2 };
|
||||
case "view-ddl":
|
||||
return { label: t("contextMenu.viewDdl"), action: () => emitContextObjectAction(action), disabled, icon: FileCode };
|
||||
}
|
||||
}
|
||||
|
||||
function executableStatementRangeStartingAt(currentView: EditorViewType, lineFrom: number) {
|
||||
@@ -917,24 +957,7 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
shortcut: shortcuts.executeSql,
|
||||
},
|
||||
]),
|
||||
{
|
||||
label: t("contextMenu.viewData"),
|
||||
action: openTableFromContextMenu,
|
||||
disabled: !contextTableName.value,
|
||||
icon: Table2,
|
||||
},
|
||||
{
|
||||
label: t("contextMenu.editStructure"),
|
||||
action: editTableStructureFromContextMenu,
|
||||
disabled: !contextTableName.value,
|
||||
icon: PencilRuler,
|
||||
},
|
||||
{
|
||||
label: t("contextMenu.viewDdl"),
|
||||
action: openTableDdlFromContextMenu,
|
||||
disabled: !contextTableName.value,
|
||||
icon: FileCode,
|
||||
},
|
||||
...queryContextObjectActions(contextObjectTarget.value?.type).map(contextObjectMenuItem),
|
||||
{ label: "", separator: true },
|
||||
{
|
||||
label: t("editor.contextMenu.copySelection"),
|
||||
@@ -1230,7 +1253,7 @@ function completionTablesMatch(left: { name: string; schema?: string | null }, r
|
||||
return left.schema.toLowerCase() === right.schema.toLowerCase();
|
||||
}
|
||||
|
||||
async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promise<{ name: string; schema?: string; type?: "table" | "view" } | null> {
|
||||
async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promise<SqlCompletionTable | null> {
|
||||
if (!props.connectionId || props.database == null) return null;
|
||||
const target = completionMetadataTarget(table);
|
||||
if (!target) return null;
|
||||
@@ -1373,7 +1396,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
|
||||
let table = matchTable(qualifiedTableLookup, cachedTables) ?? matchTable(tableLookupName, cachedTables) ?? matchTable(identifier, cachedTables) ?? matchTable(name, cachedTables);
|
||||
if (!table && !usesLocalOnlyCompletionMetadata()) {
|
||||
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, semanticTarget?.schema ?? props.schema, false, props.schema);
|
||||
cachedTables = [...cachedTables, ...hoverTables];
|
||||
cachedTables = mergeCompletionTables(cachedTables, hoverTables);
|
||||
table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
|
||||
}
|
||||
if (table && !semanticQualifierIsRowSource && (!qualifier || table.schema?.toLowerCase() === qualifier.toLowerCase() || table.name === name)) {
|
||||
@@ -2332,7 +2355,9 @@ function mergeCompletionTables(existing: SqlCompletionTable[], incoming: SqlComp
|
||||
indexes.set(key, merged.length);
|
||||
merged.push(table);
|
||||
} else {
|
||||
merged[index] = { ...merged[index], ...table };
|
||||
const existing = merged[index];
|
||||
// Preserve the more specific tree type if an older metadata endpoint reports a materialized view as VIEW.
|
||||
merged[index] = { ...existing, ...table, type: mergeSqlObjectNavigationType(existing.type, table.type) };
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
@@ -2644,7 +2669,7 @@ function mergeCompletionObjects(existing: SqlCompletionObject[], incoming: SqlCo
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function refreshCompletionCache() {
|
||||
function refreshCompletionCache() {
|
||||
cachedTables = [];
|
||||
cachedCompletionObjects = [];
|
||||
cachedColumnsByTable.clear();
|
||||
@@ -3673,7 +3698,7 @@ function scrollCursorIntoView() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition, previewStatementRange });
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition, previewStatementRange, refreshCompletionCache });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -150,9 +150,10 @@ const emit = defineEmits<{
|
||||
sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode];
|
||||
executeSql: [sql: string];
|
||||
clickTable: [target: SqlObjectNavigationTarget];
|
||||
viewTableData: [tableName: string];
|
||||
viewTableDdl: [tableName: string];
|
||||
editTableStructure: [tableName: string];
|
||||
viewTableData: [target: SqlObjectNavigationTarget];
|
||||
viewTableDdl: [target: SqlObjectNavigationTarget];
|
||||
editTableStructure: [target: SqlObjectNavigationTarget];
|
||||
openObjectSource: [target: SqlObjectNavigationTarget, initialEditing: boolean];
|
||||
openObjectTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }];
|
||||
objectSchemaChange: [schema: string | undefined];
|
||||
objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport];
|
||||
@@ -656,16 +657,20 @@ function onHandleClickTable(target: SqlObjectNavigationTarget) {
|
||||
emit("clickTable", target);
|
||||
}
|
||||
|
||||
function onHandleViewTableData(tableName: string) {
|
||||
emit("viewTableData", tableName);
|
||||
function onHandleViewTableData(target: SqlObjectNavigationTarget) {
|
||||
emit("viewTableData", target);
|
||||
}
|
||||
|
||||
function onHandleViewTableDdl(tableName: string) {
|
||||
emit("viewTableDdl", tableName);
|
||||
function onHandleViewTableDdl(target: SqlObjectNavigationTarget) {
|
||||
emit("viewTableDdl", target);
|
||||
}
|
||||
|
||||
function onHandleEditTableStructure(tableName: string) {
|
||||
emit("editTableStructure", tableName);
|
||||
function onHandleEditTableStructure(target: SqlObjectNavigationTarget) {
|
||||
emit("editTableStructure", target);
|
||||
}
|
||||
|
||||
function onHandleOpenObjectSource(target: SqlObjectNavigationTarget, initialEditing: boolean) {
|
||||
emit("openObjectSource", target, initialEditing);
|
||||
}
|
||||
|
||||
function onHandleCloseColumnPanel() {
|
||||
@@ -683,6 +688,12 @@ function focusSearch(): boolean {
|
||||
return dataGridRef.value?.focusSearch() ?? false;
|
||||
}
|
||||
|
||||
function refreshQueryEditorCompletionCache(): boolean {
|
||||
if (props.activeTab.mode !== "query" || !queryEditorRef.value) return false;
|
||||
queryEditorRef.value.refreshCompletionCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
function refreshData(): boolean {
|
||||
if (props.activeTab.mode === "etcd") return etcdKeyBrowserRef.value?.refresh?.() ?? false;
|
||||
if (props.activeTab.mode === "zookeeper") return zookeeperKeyBrowserRef.value?.refresh?.() ?? false;
|
||||
@@ -773,7 +784,7 @@ function applyTableStructureChanges() {
|
||||
return tableStructureEditorRef.value?.applyChanges() ?? Promise.resolve(false);
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExecute, pasteClipboardAsSqlInCondition, applyTableStructureChanges });
|
||||
defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, handleModRTarget, requestQueryEditorExecute, pasteClipboardAsSqlInCondition, applyTableStructureChanges });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -821,6 +832,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
@view-table-data="onHandleViewTableData"
|
||||
@edit-table-structure="onHandleEditTableStructure"
|
||||
@view-table-ddl="onHandleViewTableDdl"
|
||||
@open-object-source="onHandleOpenObjectSource"
|
||||
@click-column="onHandleClickColumn"
|
||||
@close-column-panel="onHandleCloseColumnPanel"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user