Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 : import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
3 : import { Transaction, StateField, RangeSetBuilder } from '@codemirror/state';
4 :
5 : import { DashQLProcessorPlugin, DashQLScriptKey } from './dashql_processor.js';
6 : import { createScannerHighlightPlugin } from './dashql_decorations_standalone.js';
7 : import { FocusType, SemanticUserFocus } from '../scripts/focus.js';
8 : import { editorDiagnostics } from './dashql_script_references.js';
9 :
10 : import './dashql_decorations.css';
11 :
12 1 : const CursorTableReference = Decoration.mark({
13 1 : class: 'dashql-tableref-cursor',
14 1 : });
15 1 : const FocusedTableReferenceDecoration = Decoration.mark({
16 1 : class: 'dashql-tableref-focus',
17 1 : });
18 1 : const ResolvedTableReferenceDecoration = Decoration.mark({
19 1 : class: 'dashql-tableref-resolved',
20 1 : });
21 1 : const UnresolvedTableReferenceDecoration = Decoration.mark({
22 1 : class: 'dashql-tableref-unresolved',
23 1 : });
24 1 : const CursorColumnReference = Decoration.mark({
25 1 : class: 'dashql-colref-cursor',
26 1 : });
27 1 : const FocusedColumnReferenceDecoration = Decoration.mark({
28 1 : class: 'dashql-colref-focus',
29 1 : });
30 1 : const ResolvedColumnReferenceDecoration = Decoration.mark({
31 1 : class: 'dashql-colref-resolved',
32 1 : });
33 1 : const UnresolvedColumnReferenceDecoration = Decoration.mark({
34 1 : class: 'dashql-colref-unresolved',
35 1 : });
36 1 : const FunctionReferenceDecoration = Decoration.mark({
37 1 : class: 'dashql-function-ref',
38 1 : });
39 1 : const ErrorDecoration = Decoration.mark({
40 1 : class: 'dashql-error',
41 1 : });
42 1 : const WarningDecoration = Decoration.mark({
43 1 : class: 'dashql-warning',
44 1 : });
45 :
46 1 : function buildDecorationsFromErrors(
47 1 : diagnostics: dashql.buffers.editor.EditorDiagnosticT[] | null,
48 1 : ): DecorationSet {
49 1 : const builder = new RangeSetBuilder<Decoration>();
50 1 : const decorations: DecorationInfo[] = [];
51 1 : for (const diagnostic of diagnostics ?? []) {
52 1 : const span = diagnostic.textSpan;
53 1 : if (span == null) continue;
54 1 : decorations.push({
55 1 : from: Number(span.offset),
56 1 : to: Number(span.offset + span.length),
57 1 : decoration: diagnostic.severity === dashql.buffers.editor.EditorDiagnosticSeverity.WARNING
58 1 : ? WarningDecoration
59 1 : : ErrorDecoration,
60 1 : });
61 1 : }
62 :
63 1 : decorations.sort((l: DecorationInfo, r: DecorationInfo) => {
64 0 : return l.from - r.from;
65 1 : });
66 1 : for (const deco of decorations) {
67 1 : builder.add(deco.from, deco.to, deco.decoration);
68 : }
69 1 : return builder.finish();
70 1 : }
71 :
72 1 : function buildDecorationsFromAnalysis(
73 1 : update: dashql.buffers.editor.EditorUpdateT | null | undefined,
74 1 : ): DecorationSet {
75 1 : const builder = new RangeSetBuilder<Decoration>();
76 1 : const decorations: DecorationInfo[] = [];
77 1 : for (const semantic of update?.semanticSpans ?? []) {
78 1 : const span = semantic.textSpan;
79 1 : if (span == null) continue;
80 1 : const resolved = semantic.resolution === dashql.buffers.editor.EditorSemanticResolution.RESOLVED;
81 1 : let decoration: Decoration;
82 1 : switch (semantic.kind) {
83 1 : case dashql.buffers.editor.EditorSemanticReferenceKind.TABLE:
84 1 : decoration = resolved ? ResolvedTableReferenceDecoration : UnresolvedTableReferenceDecoration;
85 1 : break;
86 1 : case dashql.buffers.editor.EditorSemanticReferenceKind.COLUMN:
87 1 : decoration = resolved ? ResolvedColumnReferenceDecoration : UnresolvedColumnReferenceDecoration;
88 1 : break;
89 1 : case dashql.buffers.editor.EditorSemanticReferenceKind.FUNCTION:
90 1 : decoration = FunctionReferenceDecoration;
91 : break;
92 1 : }
93 1 : decorations.push({
94 1 : from: Number(span.offset),
95 1 : to: Number(span.offset + span.length),
96 1 : decoration,
97 1 : });
98 1 : }
99 1 : decorations.sort((l: DecorationInfo, r: DecorationInfo) => {
100 1 : return l.from - r.from;
101 1 : });
102 1 : for (const deco of decorations) {
103 1 : builder.add(deco.from, deco.to, deco.decoration);
104 : }
105 1 : return builder.finish();
106 1 : }
107 :
108 1 : function buildDecorationsFromFocus(
109 1 : scriptKey: DashQLScriptKey | null,
110 1 : update: dashql.buffers.editor.EditorUpdateT | null | undefined,
111 1 : derivedFocus: SemanticUserFocus | null,
112 1 : ): DecorationSet {
113 1 : const builder = new RangeSetBuilder<Decoration>();
114 1 : const decorations: DecorationInfo[] = [];
115 :
116 1 : const spans = update?.semanticSpans ?? [];
117 :
118 : // Build decorations for column refs of targeting the primary table
119 1 : for (const [refId, focusType] of derivedFocus?.scriptColumnRefs ?? []) {
120 1 : const externalId = dashql.ExternalObjectID.getOrigin(refId);
121 1 : const objectId = dashql.ExternalObjectID.getObject(refId);
122 1 : if (externalId !== scriptKey) {
123 1 : continue;
124 : }
125 : // XXX invalidate focused table refs at write front
126 1 : const ts = spans.find(span => span.kind === dashql.buffers.editor.EditorSemanticReferenceKind.COLUMN
127 1 : && span.referenceId === objectId)?.textSpan;
128 1 : if (ts == null) continue;
129 :
130 : // Get decoration
131 1 : let decoration: Decoration;
132 1 : switch (focusType) {
133 1 : case FocusType.COLUMN_REF_UNDER_CURSOR:
134 1 : decoration = CursorColumnReference;
135 1 : break;
136 1 : default:
137 1 : decoration = FocusedColumnReferenceDecoration;
138 : break;
139 1 : }
140 1 : decorations.push({
141 1 : from: Number(ts.offset),
142 1 : to: Number(ts.offset + ts.length),
143 1 : decoration: decoration,
144 1 : });
145 1 : }
146 :
147 : // Build decorations for table refs targeting the primary table
148 1 : for (const [refId, focusType] of derivedFocus?.scriptTableRefs ?? []) {
149 1 : const externalId = dashql.ExternalObjectID.getOrigin(refId);
150 1 : const objectId = dashql.ExternalObjectID.getObject(refId);
151 1 : if (externalId !== scriptKey) {
152 1 : continue;
153 : }
154 : // XXX invalidate focused table refs at write front
155 1 : const ts = spans.find(span => span.kind === dashql.buffers.editor.EditorSemanticReferenceKind.TABLE
156 1 : && span.referenceId === objectId)?.textSpan;
157 1 : if (ts == null) continue;
158 :
159 : // Get decoration
160 1 : let decoration: Decoration;
161 1 : switch (focusType) {
162 1 : case FocusType.TABLE_REF_UNDER_CURSOR:
163 1 : decoration = CursorTableReference;
164 1 : break;
165 1 : default:
166 1 : decoration = FocusedTableReferenceDecoration;
167 : break;
168 1 : }
169 1 : decorations.push({
170 1 : from: Number(ts.offset),
171 1 : to: Number(ts.offset + ts.length),
172 1 : decoration: decoration,
173 1 : });
174 1 : }
175 1 : decorations.sort((l: DecorationInfo, r: DecorationInfo) => {
176 1 : return l.from - r.from;
177 1 : });
178 1 : for (const deco of decorations) {
179 1 : builder.add(deco.from, deco.to, deco.decoration);
180 : }
181 1 : return builder.finish();
182 1 : }
183 :
184 :
185 : interface DecorationInfo {
186 : from: number;
187 : to: number;
188 : decoration: Decoration;
189 : }
190 :
191 :
192 : interface ScriptDecorationState {
193 : decorations: DecorationSet;
194 : /// The spans the decorations were built from, an update that omits an unchanged analysis keeps them
195 : spans: unknown[] | null;
196 : }
197 :
198 : /// Syntax highlighting derived from the portable editor projection.
199 : /// Viewport-limited: decorates only the visible spans.
200 1 : const ScannerHighlightPlugin = createScannerHighlightPlugin(
201 1 : view => view.state.field(DashQLProcessorPlugin).editorUpdate ?? null,
202 1 : );
203 :
204 : /// Decorations for scanner, parser or analyzer errors in the DashQL script
205 1 : const ErrorDecorationField: StateField<ScriptDecorationState> = StateField.define<ScriptDecorationState>({
206 1 : create: () => {
207 : const config: ScriptDecorationState = {
208 1 : decorations: new RangeSetBuilder<Decoration>().finish(),
209 1 : spans: null,
210 : };
211 1 : return config;
212 1 : },
213 1 : update: (state: ScriptDecorationState, transaction: Transaction) => {
214 : // Spans untouched?
215 1 : const spans = editorDiagnostics(transaction.state);
216 1 : if (spans === state.spans) {
217 1 : return state;
218 : }
219 : // Rebuild decorations
220 1 : const s = { ...state };
221 1 : s.spans = spans;
222 1 : s.decorations = buildDecorationsFromErrors(spans);
223 1 : return s;
224 1 : },
225 1 : });
226 :
227 1 : const AnalyzerDecorationsField: StateField<ScriptDecorationState> = StateField.define<ScriptDecorationState>({
228 1 : create: () => {
229 : const config: ScriptDecorationState = {
230 1 : decorations: new RangeSetBuilder<Decoration>().finish(),
231 1 : spans: null,
232 : };
233 1 : return config;
234 1 : },
235 1 : update: (state: ScriptDecorationState, transaction: Transaction) => {
236 : // Spans untouched?
237 1 : const processor = transaction.state.field(DashQLProcessorPlugin);
238 1 : const spans = processor.editorUpdate?.semanticSpans ?? null;
239 1 : if (spans === state.spans) {
240 1 : return state;
241 : }
242 : // Rebuild decorations
243 1 : const s = { ...state };
244 1 : s.spans = spans;
245 1 : s.decorations = buildDecorationsFromAnalysis(processor.editorUpdate);
246 1 : return s;
247 1 : },
248 1 : });
249 :
250 : interface FocusDecorationState {
251 : scriptKey: DashQLScriptKey | null;
252 : decorations: DecorationSet;
253 : /// The semantic spans the decorations were built from
254 : semanticSpans: dashql.buffers.editor.EditorSemanticSpanT[] | null;
255 : derivedFocus: SemanticUserFocus | null;
256 : }
257 :
258 : /// Decorations derived from the user focus
259 1 : const FocusDecorationField: StateField<FocusDecorationState> = StateField.define<FocusDecorationState>({
260 : // Create the initial state
261 1 : create: () => {
262 : const config: FocusDecorationState = {
263 1 : scriptKey: null,
264 1 : decorations: new RangeSetBuilder<Decoration>().finish(),
265 1 : semanticSpans: null,
266 1 : derivedFocus: null,
267 : };
268 1 : return config;
269 1 : },
270 : // Mirror the DashQL state
271 1 : update: (state: FocusDecorationState, transaction: Transaction) => {
272 : // Scanned program untouched?
273 1 : const processor = transaction.state.field(DashQLProcessorPlugin);
274 1 : const semanticSpans = processor.editorUpdate?.semanticSpans ?? null;
275 1 : if (
276 1 : processor.scriptKey === state.scriptKey &&
277 1 : semanticSpans === state.semanticSpans &&
278 1 : processor.derivedFocus === state.derivedFocus
279 : ) {
280 1 : return state;
281 : }
282 : // Rebuild decorations from the portable UTF-16 projection.
283 1 : const s = { ...state };
284 1 : s.scriptKey = processor.scriptKey;
285 1 : s.semanticSpans = semanticSpans;
286 1 : s.derivedFocus = processor.derivedFocus;
287 1 : s.decorations = buildDecorationsFromFocus(
288 1 : s.scriptKey,
289 1 : processor.editorUpdate,
290 1 : s.derivedFocus,
291 1 : );
292 1 : return s;
293 1 : },
294 1 : });
295 :
296 1 : const ErrorDecorations = EditorView.decorations.from(ErrorDecorationField, state => state.decorations);
297 1 : const AnalyzerDecorations = EditorView.decorations.from(AnalyzerDecorationsField, state => state.decorations);
298 1 : const FocusDecorations = EditorView.decorations.from(FocusDecorationField, state => state.decorations);
299 :
300 : export const DashQLScannerDecorationPlugin = [ScannerHighlightPlugin];
301 :
302 : /// Bundle the decoration extensions
303 1 : export const DashQLDecorationPlugin = [ScannerHighlightPlugin, ErrorDecorationField, ErrorDecorations, AnalyzerDecorationsField, AnalyzerDecorations, FocusDecorationField, FocusDecorations];
|