Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 :
3 : import { foldedRanges } from '@codemirror/language';
4 : import { gutter, GutterMarker } from '@codemirror/view';
5 : import { Transaction, StateField, RangeSet, RangeSetBuilder } from '@codemirror/state';
6 :
7 : import { editorDiagnostics } from './dashql_script_references.js';
8 : import { DashQLProcessorPlugin, DashQLScriptKey } from './dashql_processor.js';
9 : import { findVegaLiteSpecs, isFolded, toggleVegaLiteSpecAtLine, VegaLiteFoldMarker } from './dashql_vis_folding.js';
10 :
11 : import icons from '@ankoh/dashql-svg-symbols';
12 :
13 : import './dashql_gutters.css';
14 :
15 1 : class ErrorMarker extends GutterMarker {
16 0 : eq(other: GutterMarker): boolean {
17 0 : return other instanceof ErrorMarker;
18 0 : }
19 1 : toDOM() {
20 1 : const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
21 1 : svg.setAttribute('class', 'dashql-gutter-error');
22 1 : svg.setAttribute('width', '14px');
23 1 : svg.setAttribute('height', '14px');
24 1 : const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
25 1 : use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `${icons}#close_circle`);
26 1 : svg.appendChild(use);
27 1 : return svg;
28 1 : }
29 1 : }
30 :
31 1 : const ERROR_MARKER = new ErrorMarker();
32 :
33 : interface State {
34 : scriptKey: DashQLScriptKey | null;
35 : /// The diagnostics the error lines were derived from, an update that omits an unchanged analysis keeps them
36 : diagnostics: dashql.buffers.editor.EditorDiagnosticT[] | null;
37 : /// The Vega-Lite specs and folds the fold markers were derived from
38 : vegaliteSpecs: dashql.buffers.editor.EditorTextSpanT[] | null;
39 : folds: ReturnType<typeof foldedRanges> | null;
40 : /// The error and fold markers share one gutter to keep it narrow
41 : markers: RangeSet<GutterMarker>;
42 : }
43 :
44 1 : const GutterState: StateField<State> = StateField.define<State>({
45 : // Create the initial state
46 1 : create: () => ({
47 1 : scriptKey: null,
48 1 : diagnostics: null,
49 1 : vegaliteSpecs: null,
50 1 : folds: null,
51 1 : markers: RangeSet.empty,
52 1 : }),
53 1 : update: (state: State, transaction: Transaction) => {
54 : // Program and folds untouched?
55 1 : const processor = transaction.state.field(DashQLProcessorPlugin);
56 1 : const diagnostics = editorDiagnostics(transaction.state);
57 1 : const vegaliteSpecs = processor.editorUpdate?.vegaliteSpecs ?? null;
58 1 : const folds = foldedRanges(transaction.state);
59 1 : if (
60 1 : processor.scriptKey === state.scriptKey &&
61 1 : diagnostics === state.diagnostics &&
62 1 : vegaliteSpecs === state.vegaliteSpecs &&
63 1 : folds === state.folds
64 : ) {
65 1 : return state;
66 : }
67 :
68 : // The analysis may lag behind the text
69 1 : const doc = transaction.state.doc;
70 1 : const lineAt = (offset: number) => doc.lineAt(Math.min(offset, doc.length)).from;
71 1 : const errorLines: Set<number> = new Set();
72 1 : for (const diagnostic of diagnostics ?? []) {
73 1 : if (diagnostic.severity !== dashql.buffers.editor.EditorDiagnosticSeverity.ERROR) continue;
74 1 : const loc = diagnostic.textSpan;
75 1 : if (loc == null) continue;
76 1 : errorLines.add(lineAt(Number(loc.offset)));
77 1 : }
78 1 : const foldLines: Map<number, boolean> = new Map();
79 1 : for (const spec of findVegaLiteSpecs(processor.editorUpdate)) {
80 1 : foldLines.set(lineAt(spec.from), !isFolded(transaction.state, spec));
81 : }
82 1 : const builder = new RangeSetBuilder<GutterMarker>();
83 1 : for (const line of [...new Set([...errorLines, ...foldLines.keys()])].sort((l, r) => l - r)) {
84 1 : const open = foldLines.get(line);
85 1 : const error = errorLines.has(line);
86 1 : builder.add(line, line, open == null ? ERROR_MARKER : new VegaLiteFoldMarker(open, error));
87 1 : }
88 1 : return {
89 1 : scriptKey: processor.scriptKey,
90 1 : diagnostics,
91 1 : vegaliteSpecs,
92 1 : folds,
93 1 : markers: builder.finish(),
94 1 : };
95 1 : },
96 1 : });
97 :
98 1 : const GutterExtension = gutter({
99 1 : class: 'dashql-gutter-markers',
100 1 : markers: view => view.state.field(GutterState).markers,
101 1 : initialSpacer() {
102 1 : return ERROR_MARKER;
103 1 : },
104 1 : domEventHandlers: {
105 1 : click: (view, line) => toggleVegaLiteSpecAtLine(view, line.from, line.to),
106 1 : },
107 1 : });
108 :
109 1 : export const DashQLGutterPlugin = [GutterState, GutterExtension];
|