Editor: view-scoped Extract-to-Dataset for inline and self-defined data

This commit is contained in:
2026-06-29 02:08:59 +03:00
parent 8cad80f738
commit 6656811b8e
16 changed files with 910 additions and 180 deletions
+82
View File
@@ -0,0 +1,82 @@
/**
* Extract-to-Dataset as an editor action (spec §03F; docs/architecture/08 →
* editor augmentation) — the data-facing counterpart to the wrap/config actions.
*
* Resolves the embedded data of the **view the cursor sits in** (the nearest
* enclosing `data` binding, honoring Vega-Lite's parent→child inheritance), seeds
* the Extract modal with it, and opens the modal. The cursor picks which view to
* extract; a single-view spec resolves to the root binding from any cursor, so the
* common case needs no thought. Two binding shapes are liftable — a view's inline
* `data.values`, and a `{ name }` reference to the spec's own top-level `datasets`
* (the data lives in the `datasets` map; we pre-fill the modal with its name).
*
* The toolbar offers the action whenever *some* view has extractable data
* (`hasExtractableData`); when the focused view itself has none — it references a
* library dataset, a url, a generator — there is nothing to lift here, so guide the
* user to a view that does rather than silently extract the wrong one.
*/
import type * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { pathAtOffset } from '@core/spec-cursor';
import { dataBindingAtPath } from '@core/spec-data';
import { inlineValuesOf, selfDefinedPayloadOf } from '@core/spec-inline-data';
import { openModal } from '../modals/ModalCoordinator';
import { type ExtractTarget, useExtractStore } from '../stores/ExtractStore';
import { notify } from '../stores/NotificationStore';
// TODO(ux-second-pass): toolbar-only — no F1-palette accelerator like the sibling
// wrap/config actions, since this opens a modal rather than an in-place edit.
/** Open Extract-to-Dataset scoped to the view at the cursor. */
export function runExtract(editor: monaco.editor.IStandaloneCodeEditor): void {
const model = editor.getModel();
if (!model) return;
const text = model.getValue();
let spec: unknown;
try {
spec = JSON.parse(text);
} catch {
notify({
kind: 'info',
title: 'Cant extract yet',
message: 'Fix the JSON so the spec parses, then extract.',
});
return;
}
const position = editor.getPosition();
const offset = position ? model.getOffsetAt(position) : 0;
const binding = dataBindingAtPath(spec, pathAtOffset(text, offset));
// Inline `values` → rewrite this view's data block; a self-defined `datasets`
// reference → lift the named entry and pre-fill the modal with that name.
const inline = binding && inlineValuesOf(binding.data);
const selfDefined = binding && !inline && selfDefinedPayloadOf(spec, binding.data);
let seed: {
source: ReturnType<typeof inlineValuesOf>;
target: ExtractTarget;
name?: string;
} | null = null;
if (binding && inline) {
seed = { source: inline, target: { kind: 'inline', anchorPath: binding.anchorPath } };
} else if (binding && selfDefined) {
const datasetName = (binding.data as { name: string }).name;
seed = {
source: selfDefined,
target: { kind: 'self-defined', datasetName },
name: datasetName,
};
}
if (!seed || !seed.source) {
notify({
kind: 'info',
title: 'No data to extract here',
message: 'Place the cursor in a view with inline or embedded data to extract it.',
});
return;
}
useExtractStore.getState().begin({ source: seed.source, target: seed.target, name: seed.name });
openModal('extract');
}