mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: view-scoped data context — per-view dataset columns in hints and transforms
This commit is contained in:
@@ -43,7 +43,7 @@ import {
|
||||
} from '@core/spec-transforms';
|
||||
import { notify } from '../stores/NotificationStore';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import { boundColumns } from './active-dataset';
|
||||
import { boundColumnsAt } from './active-dataset';
|
||||
import { parseSpecObject } from './spec-config-actions';
|
||||
|
||||
/** The composition operators the wrap actions offer. */
|
||||
@@ -53,6 +53,8 @@ type WrapKind = 'layer' | 'hconcat' | 'vconcat' | 'facet' | 'repeat';
|
||||
interface Scope {
|
||||
range: monaco.Range;
|
||||
text: string;
|
||||
/** Whole-document offset of the focused view, for resolving its data binding. */
|
||||
offset: number;
|
||||
/** Column the slice starts at (0-based), so re-indented output stays aligned. */
|
||||
baseCol: number;
|
||||
}
|
||||
@@ -63,29 +65,31 @@ const isEmptyRange = (r: monaco.IRange): boolean =>
|
||||
/** The whole document, as a scope. */
|
||||
function wholeDocument(model: monaco.editor.ITextModel): Scope {
|
||||
const range = model.getFullModelRange();
|
||||
return { range, text: model.getValue(), baseCol: 0 };
|
||||
return { range, text: model.getValue(), offset: 0, baseCol: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* The slice a transform acts on: an explicit selection if present; else the view
|
||||
* the cursor sits in (core/spec-cursor); else the whole document.
|
||||
* the cursor sits in (core/spec-cursor); else the whole document. `offset` is the
|
||||
* cursor/selection-start position in the whole document, used to resolve the
|
||||
* focused view's data binding (facet/repeat field defaults).
|
||||
*/
|
||||
function resolveScope(model: monaco.editor.ITextModel, range: monaco.IRange | null): Scope {
|
||||
if (!range) return wholeDocument(model);
|
||||
if (!isEmptyRange(range)) {
|
||||
const r = monaco.Range.lift(range);
|
||||
return { range: r, text: model.getValueInRange(r), baseCol: r.startColumn - 1 };
|
||||
}
|
||||
const offset = model.getOffsetAt({
|
||||
lineNumber: range.startLineNumber,
|
||||
column: range.startColumn,
|
||||
});
|
||||
if (!isEmptyRange(range)) {
|
||||
const r = monaco.Range.lift(range);
|
||||
return { range: r, text: model.getValueInRange(r), offset, baseCol: r.startColumn - 1 };
|
||||
}
|
||||
const node = findViewRange(model.getValue(), offset);
|
||||
if (!node) return wholeDocument(model);
|
||||
const start = model.getPositionAt(node.offset);
|
||||
const end = model.getPositionAt(node.offset + node.length);
|
||||
const r = new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
|
||||
return { range: r, text: model.getValueInRange(r), baseCol: start.column - 1 };
|
||||
return { range: r, text: model.getValueInRange(r), offset, baseCol: start.column - 1 };
|
||||
}
|
||||
|
||||
/** Indent every line after the first by `baseCol`, so a scoped edit stays aligned. */
|
||||
@@ -106,8 +110,8 @@ function formatScoped(model: monaco.editor.ITextModel, scope: Scope, next: JsonO
|
||||
}
|
||||
|
||||
/** A categorical column to facet by (first nominal/ordinal), or a placeholder. */
|
||||
function defaultFacet(): { field: string; type: string } {
|
||||
const cols = boundColumns();
|
||||
function defaultFacet(text: string, offset: number): { field: string; type: string } {
|
||||
const cols = boundColumnsAt(text, offset);
|
||||
const categorical = cols.find((c) => {
|
||||
const t = defaultFieldType(c.type);
|
||||
return t === 'nominal' || t === 'ordinal';
|
||||
@@ -119,8 +123,12 @@ function defaultFacet(): { field: string; type: string } {
|
||||
}
|
||||
|
||||
/** Quantitative columns to repeat over + the channel to rewire, with fallbacks. */
|
||||
function defaultRepeat(spec: JsonObject): { fields: string[]; channel: string | null } {
|
||||
const cols = boundColumns();
|
||||
function defaultRepeat(
|
||||
spec: JsonObject,
|
||||
text: string,
|
||||
offset: number,
|
||||
): { fields: string[]; channel: string | null } {
|
||||
const cols = boundColumnsAt(text, offset);
|
||||
const numeric = cols
|
||||
.filter((c) => defaultFieldType(c.type) === 'quantitative')
|
||||
.map((c) => c.name);
|
||||
@@ -130,8 +138,12 @@ function defaultRepeat(spec: JsonObject): { fields: string[]; channel: string |
|
||||
return { fields: fields.length > 0 ? fields : ['field1', 'field2'], channel };
|
||||
}
|
||||
|
||||
/** Apply a wrap of the given kind to the parsed spec. */
|
||||
function buildNext(spec: JsonObject, kind: WrapKind): JsonObject {
|
||||
/**
|
||||
* Apply a wrap of the given kind to the parsed (scoped) spec. `text`/`offset` are
|
||||
* the whole document and the focused view's position, used to resolve that view's
|
||||
* data binding for the facet/repeat field defaults.
|
||||
*/
|
||||
function buildNext(spec: JsonObject, kind: WrapKind, text: string, offset: number): JsonObject {
|
||||
switch (kind) {
|
||||
case 'layer':
|
||||
return wrapInLayer(spec);
|
||||
@@ -140,11 +152,11 @@ function buildNext(spec: JsonObject, kind: WrapKind): JsonObject {
|
||||
case 'vconcat':
|
||||
return wrapInConcat(spec, 'v');
|
||||
case 'facet': {
|
||||
const { field, type } = defaultFacet();
|
||||
const { field, type } = defaultFacet(text, offset);
|
||||
return wrapInFacet(spec, field, type);
|
||||
}
|
||||
case 'repeat': {
|
||||
const { fields, channel } = defaultRepeat(spec);
|
||||
const { fields, channel } = defaultRepeat(spec, text, offset);
|
||||
return wrapInRepeat(spec, fields, channel);
|
||||
}
|
||||
}
|
||||
@@ -190,7 +202,8 @@ export function runWrap(editor: monaco.editor.IStandaloneCodeEditor, kind: WrapK
|
||||
const target = resolveTarget(editor);
|
||||
if (!target) return;
|
||||
const { model, scope, spec } = target;
|
||||
writeBack(editor, scope.range, formatScoped(model, scope, buildNext(spec, kind)));
|
||||
const next = buildNext(spec, kind, model.getValue(), scope.offset);
|
||||
writeBack(editor, scope.range, formatScoped(model, scope, next));
|
||||
notify({
|
||||
kind: 'success',
|
||||
title: 'View wrapped',
|
||||
@@ -381,13 +394,16 @@ export function configureSpecTransformCodeActions(): void {
|
||||
return empty;
|
||||
}
|
||||
if (!isJsonObject(spec)) return empty;
|
||||
const viewSpec = spec;
|
||||
|
||||
const fullText = model.getValue();
|
||||
const build = (kind: WrapKind) => buildNext(viewSpec, kind, fullText, scope.offset);
|
||||
const actions: monaco.languages.CodeAction[] = [
|
||||
editAction(model, scope, 'Wrap view in a layer', buildNext(spec, 'layer')),
|
||||
editAction(model, scope, 'Wrap view in horizontal concat', buildNext(spec, 'hconcat')),
|
||||
editAction(model, scope, 'Wrap view in vertical concat', buildNext(spec, 'vconcat')),
|
||||
editAction(model, scope, 'Wrap view in a facet', buildNext(spec, 'facet')),
|
||||
editAction(model, scope, 'Wrap view in a repeat', buildNext(spec, 'repeat')),
|
||||
editAction(model, scope, 'Wrap view in a layer', build('layer')),
|
||||
editAction(model, scope, 'Wrap view in horizontal concat', build('hconcat')),
|
||||
editAction(model, scope, 'Wrap view in vertical concat', build('vconcat')),
|
||||
editAction(model, scope, 'Wrap view in a facet', build('facet')),
|
||||
editAction(model, scope, 'Wrap view in a repeat', build('repeat')),
|
||||
];
|
||||
const collapsed = unwrapSingleton(spec);
|
||||
if (collapsed)
|
||||
|
||||
Reference in New Issue
Block a user