LCOV - code coverage report
Current view: top level - dashql-app/src/session/editor - dashql_decorations.ts Coverage Total Hit
Test: DashQL 915828b Lines: 99.5 % 216 215
Test Date: 2026-11-06 19:30:32

            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];
        

Generated by: LCOV version 2.0-1