feat(query): show result source ranges
This commit is contained in:
5 files changed
+248
-26
No files matched your search
@@ -249,8 +249,10 @@ let codeMirrorInsertNewlineKeepIndent: typeof import("@codemirror/commands").ins
|
||||
let codeMirrorToggleLineComment: typeof import("@codemirror/commands").toggleLineComment | null = null;
|
||||
let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSemanticDiagnostic[]> | null = null;
|
||||
let setPreviewRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let setResultSourceRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let previewRangeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let buildPreviewRangeExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildResultSourceRangeExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildRunStatementGutterExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let indentComp: import("@codemirror/state").Compartment | null = null;
|
||||
let codeMirrorIndentUnit: typeof import("@codemirror/language").indentUnit | null = null;
|
||||
@@ -491,6 +493,33 @@ function setPreviewRange(range: { from: number; to: number } | null) {
|
||||
});
|
||||
}
|
||||
|
||||
function setResultSourceRange(range: { from: number; to: number } | null) {
|
||||
if (!view.value || !setResultSourceRangeEffect) return;
|
||||
view.value.dispatch({
|
||||
effects: setResultSourceRangeEffect.of(range),
|
||||
});
|
||||
}
|
||||
|
||||
function previewStatementRange(range: { from: number; to: number } | null) {
|
||||
const currentView = view.value;
|
||||
if (!range || !currentView || !editorViewModule || !setResultSourceRangeEffect) {
|
||||
setResultSourceRange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const from = Math.max(0, Math.min(range.from, currentView.state.doc.length));
|
||||
const to = Math.max(from, Math.min(range.to, currentView.state.doc.length));
|
||||
if (from === to) {
|
||||
setResultSourceRange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
currentView.dispatch({
|
||||
selection: { anchor: from },
|
||||
effects: [setResultSourceRangeEffect.of({ from, to }), editorViewModule.EditorView.scrollIntoView(from, { y: "center" })],
|
||||
});
|
||||
}
|
||||
|
||||
function onPickerActiveIndexChange(index: number) {
|
||||
pickerActiveIndex.value = index;
|
||||
const candidate = pickerCandidates.value[index];
|
||||
@@ -2574,8 +2603,8 @@ onMounted(async () => {
|
||||
if (!editorRef.value) return;
|
||||
|
||||
const [
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, Decoration, tooltips, gutter, GutterMarker, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, ViewPlugin },
|
||||
{ EditorState, EditorSelection, Compartment, Prec, StateEffect, StateField },
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, Decoration, tooltips, gutter, GutterMarker, lineNumberMarkers, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, ViewPlugin },
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
langSql,
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField },
|
||||
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, history, defaultKeymap, historyKeymap },
|
||||
@@ -2681,6 +2710,7 @@ onMounted(async () => {
|
||||
return Decoration.set([Decoration.mark({ class: "cm-db-execution-preview" }).range(range.from, range.to)]);
|
||||
}
|
||||
}
|
||||
if (transaction.docChanged || transaction.selection) return Decoration.none;
|
||||
return decorations;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
@@ -2688,6 +2718,41 @@ onMounted(async () => {
|
||||
return field;
|
||||
};
|
||||
|
||||
class ResultSourceLineNumberMarker extends GutterMarker {
|
||||
elementClass = "cm-db-result-source-line-number";
|
||||
}
|
||||
|
||||
const resultSourceLineNumberMarker = new ResultSourceLineNumberMarker();
|
||||
setResultSourceRangeEffect = StateEffect.define<{ from: number; to: number } | null>();
|
||||
buildResultSourceRangeExtension = () => {
|
||||
const effectType = setResultSourceRangeEffect!;
|
||||
const markersForRange = (state: import("@codemirror/state").EditorState, range: { from: number; to: number }) => {
|
||||
const from = Math.max(0, Math.min(range.from, state.doc.length));
|
||||
const to = Math.max(from, Math.min(range.to, state.doc.length));
|
||||
const startLine = state.doc.lineAt(from);
|
||||
const endLine = state.doc.lineAt(Math.max(from, to - 1));
|
||||
const markers = Array.from({ length: endLine.number - startLine.number + 1 }, (_, index) => resultSourceLineNumberMarker.range(state.doc.line(startLine.number + index).from));
|
||||
return RangeSet.of(markers);
|
||||
};
|
||||
|
||||
const field = StateField.define({
|
||||
create() {
|
||||
return RangeSet.empty;
|
||||
},
|
||||
update(markers, transaction) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(effectType)) {
|
||||
return effect.value ? markersForRange(transaction.state, effect.value) : RangeSet.empty;
|
||||
}
|
||||
}
|
||||
if (transaction.docChanged || transaction.selection) return RangeSet.empty;
|
||||
return markers;
|
||||
},
|
||||
provide: (field) => lineNumberMarkers.from(field),
|
||||
});
|
||||
return field;
|
||||
};
|
||||
|
||||
buildSqlSignatureExtension = () =>
|
||||
showTooltip.compute(["doc", "selection"], (currentState) => {
|
||||
const signature = getSqlFunctionSignatureHelp(currentState.doc.toString(), currentState.selection.main.head);
|
||||
@@ -2878,6 +2943,7 @@ onMounted(async () => {
|
||||
requestTableColumns: requestInsertValueHintTableColumns,
|
||||
}),
|
||||
previewRangeComp.of(buildPreviewRangeExtension()),
|
||||
buildResultSourceRangeExtension(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: "'", run: handleSqlSingleQuote },
|
||||
@@ -3462,7 +3528,7 @@ function scrollCursorIntoView() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition });
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition, previewStatementRange });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -3495,6 +3561,15 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pa
|
||||
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35));
|
||||
}
|
||||
|
||||
:deep(.cm-lineNumbers .cm-db-result-source-line-number) {
|
||||
color: rgb(126 34 206) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(.dark) :deep(.cm-lineNumbers .cm-db-result-source-line-number) {
|
||||
color: rgb(216 180 254) !important;
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive";
|
||||
import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
|
||||
import { isTableDataEditable } from "@/lib/table/tableEditing";
|
||||
@@ -729,6 +729,14 @@ function toggleResultAutoSave() {
|
||||
toast(t(enabled ? "tabs.autoKeepResultsEnabled" : "tabs.autoKeepResultsDisabled"), 2500);
|
||||
}
|
||||
|
||||
function selectResultItem(item: (typeof visibleResultItems.value)[number]) {
|
||||
queryStore.setActiveResultIndex(props.activeTab.id, item.index);
|
||||
emit("update:activeOutputView", "result");
|
||||
nextTick(() => {
|
||||
queryEditorRef.value?.previewStatementRange(resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value) ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
function handleModRTarget(target: Element): boolean {
|
||||
if (target.closest("[data-query-editor-root]")) return queryEditorRef.value?.openReplace() ?? false;
|
||||
if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false;
|
||||
@@ -852,20 +860,11 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
<div class="result-tab-scrollbar__thumb" :style="resultTabsScrollbarThumbStyle" />
|
||||
</div>
|
||||
<div ref="resultTabsScrollerRef" class="result-tab-scroll flex h-full items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :style="resultTabsScrollerStyle" @scroll="updateResultTabsScrollbar" @wheel="onResultTabsWheel">
|
||||
<Button
|
||||
v-for="item in visibleResultItems"
|
||||
:key="item.index"
|
||||
size="sm"
|
||||
:variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'"
|
||||
class="h-6 max-w-48 shrink-0 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-xs"
|
||||
:title="item.title || item.label || t('tabs.resultN', { n: item.n })"
|
||||
@click="
|
||||
queryStore.setActiveResultIndex(activeTab.id, item.index);
|
||||
emit('update:activeOutputView', 'result');
|
||||
"
|
||||
>
|
||||
{{ item.label || t("tabs.resultN", { n: item.n }) }}
|
||||
</Button>
|
||||
<LightTooltip v-for="item in visibleResultItems" :key="item.index" :text="item.label || item.title || t('tabs.resultN', { n: item.n })" :disabled="!item.labelTruncated && !(!item.label && item.title)" :delay="150" :close-delay="0" nowrap>
|
||||
<Button size="sm" :variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'" class="h-6 min-w-0 max-w-48 shrink-0 px-2 text-xs" :aria-label="item.label || t('tabs.resultN', { n: item.n })" @click="selectResultItem(item)">
|
||||
<span class="block min-w-0 max-w-44 whitespace-nowrap">{{ item.displayLabel || item.label || t("tabs.resultN", { n: item.n }) }}</span>
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in new issue
Block a user