All files / src/cm6 lint-source.ts

100% Statements 15/15
100% Branches 7/7
100% Functions 5/5
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59                              173x 30x 30x   30x   28x   28x                         27x 27x   27x                   27x   25x   1x   1x      
import { type Diagnostic as CMDiagnostic, type LintSource } from '@codemirror/lint';
import {
  SpelDiagnosticEngine,
  type ContextSchema,
  type SpelDiagnostic,
  DiagnosticSeverity,
} from '@agentix-e/spel-ts';
 
/**
 * Adapter: spel-ts DiagnosticEngine → CM6 LintSource.
 *
 * Runs syntax + semantic + context checks on each change
 * and maps SpelDiagnostic[] to CM6 Diagnostic[].
 */
export function spelLint(getContextSchema?: () => ContextSchema | null): LintSource {
  return (view) => {
    const expression = view.state.sliceDoc();
    const schema = getContextSchema?.() ?? undefined;
 
    if (expression.trim().length === 0) return [];
 
    const diagnostics = SpelDiagnosticEngine.validate(expression, schema);
 
    return diagnostics.map((diagnostic) => mapToCM6Diagnostic(diagnostic, view.state.doc.length));
  };
}
 
/**
 * Map an engine diagnostic to a CM6 one, clamped to the document.
 *
 * CodeMirror rejects a range outside the document and a rejected decoration takes
 * out the whole lint run, so a single out-of-range position would cost the user every
 * mark instead of one. Clamping keeps the annotations that are in range and pins an
 * over-long one to the end of the text, where the problem usually is.
 */
export function mapToCM6Diagnostic(diagnostic: SpelDiagnostic, docLength: number): CMDiagnostic {
  const from = Math.max(0, Math.min(diagnostic.from, docLength));
  const to = Math.max(from, Math.min(diagnostic.to, docLength));
 
  return {
    from,
    to,
    message: diagnostic.message,
    severity: mapSeverity(diagnostic.severity),
    source: diagnostic.code,
  };
}
 
function mapSeverity(sev: DiagnosticSeverity): 'error' | 'warning' | 'info' {
  switch (sev) {
    case DiagnosticSeverity.ERROR:
      return 'error';
    case DiagnosticSeverity.WARNING:
      return 'warning';
    case DiagnosticSeverity.INFO:
      return 'info';
  }
}