All files / src/cm6 spel-grammar.ts

100% Statements 136/136
98.79% Branches 82/83
100% Functions 5/5
100% Lines 136/136

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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189                    1x 1x           1x 259x   259x 6x 259x 5x 259x 259x 259x 259x 259x 4x   259x 259x 259x 259x 259x 49x 259x 5x   259x   71x   259x 259x 259x 259x 259x 259x 259x 259x 259x 26x   259x 259x 259x 259x 259x 259x 259x 259x 259x 11x   259x 259x 259x 32x   259x 259x 259x 259x 259x 259x 259x 259x 259x 259x 259x 259x 259x 45x   259x 259x 259x 259x 3x   259x 1x 259x 1x 259x 259x     1x   141x 141x           1x     143x 143x 143x 143x   143x   143x   258x 79x 79x 79x 79x 79x     2x 2x 2x 2x 2x 77x 77x   79x   246x 246x     246x 70x 70x   27x 27x 27x 27x 27x 27x 27x 70x   7x 7x 7x 7x 7x 7x 7x 70x   212x 212x 77x     257x 246x 246x 242x 242x 242x 242x 4x 4x     14x 14x 14x 258x   143x 143x  
/**
 * SpEL grammar parser for CodeMirror 6 — StreamLanguage adapter.
 *
 * Uses @agentix-e/spel-ts Tokenizer for precise token-level parsing
 * mapped to CM6 StreamParser tokens. Provides accurate syntax highlighting
 * matching spel-ts v1.1.0 lexer behavior without a full Lezer grammar build step.
 *
 * For incremental parsing and richer editor features, a full Lezer grammar
 * can be added later via @lezer/generator build pipeline.
 */
import { type StringStream, StreamLanguage } from '@codemirror/language';
import { Tokenizer as SpelTokenizer, TokenKind } from '@agentix-e/spel-ts';
 
/**
 * Map spel-ts TokenKind to CM6 highlight style.
 * Exported for testing.
 */
export function tokenKindToStyle(kind: TokenKind): string {
  switch (kind) {
    // Keywords
    case TokenKind.LITERAL_NULL:
      return 'keyword';
    case TokenKind.LITERAL_BOOLEAN:
      return 'bool';
    case TokenKind.MATCHES:
    case TokenKind.BETWEEN:
    case TokenKind.INSTANCEOF:
    case TokenKind.MOD:
    case TokenKind.NEW:
      return 'keyword';
    // Literals
    case TokenKind.LITERAL_INT:
    case TokenKind.LITERAL_LONG:
    case TokenKind.LITERAL_FLOAT:
    case TokenKind.LITERAL_DOUBLE:
    case TokenKind.LITERAL_HEX:
      return 'number';
    case TokenKind.LITERAL_STRING:
      return 'string';
    // Variables
    case TokenKind.IDENTIFIER:
      // Could be a variable reference if preceded by #, handled below
      return 'variableName';
    // Operators
    case TokenKind.PLUS:
    case TokenKind.MINUS:
    case TokenKind.STAR:
    case TokenKind.SLASH:
    case TokenKind.PERCENT:
    case TokenKind.POWER:
    case TokenKind.INC:
    case TokenKind.DEC:
    case TokenKind.ASSIGN:
      return 'operator';
    // Comparison
    case TokenKind.EQ:
    case TokenKind.NE:
    case TokenKind.LT:
    case TokenKind.LE:
    case TokenKind.GT:
    case TokenKind.GE:
    case TokenKind.AND:
    case TokenKind.OR:
    case TokenKind.NOT:
      return 'operator';
    // Type/Bean references
    case TokenKind.HASH:
    case TokenKind.AT:
    case TokenKind.AMP_AT:
      return 'typeName';
    // Punctuation
    case TokenKind.LPAREN:
    case TokenKind.RPAREN:
    case TokenKind.LBRACKET:
    case TokenKind.RBRACKET:
    case TokenKind.LBRACE:
    case TokenKind.RBRACE:
    case TokenKind.COMMA:
    case TokenKind.COLON:
    case TokenKind.DOT:
    case TokenKind.SAFE_NAV:
    case TokenKind.QMARK:
    case TokenKind.ELVIS:
    case TokenKind.DOTDOT:
      return 'punctuation';
    // Selection/Projection
    case TokenKind.PROJECTION:
    case TokenKind.SELECTION:
    case TokenKind.SELECT_FIRST:
    case TokenKind.SELECT_LAST:
      return 'operatorKeyword';
    // Type
    case TokenKind.TYPE_START:
      return 'typeName';
    default:
      return '';
  }
}
 
/** Create a StreamLanguage-based SpEL language for CodeMirror 6 */
export function createSpelStreamParser() {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return StreamLanguage.define(createTokenParser() as any);
}
 
/**
 * Create the token parser spec for StreamLanguage.define.
 * Exported for testing — allows direct invocation of the token function.
 */
export function createTokenParser(): {
  startState: () => null;
  token: (stream: StringStream) => string | null;
} {
  let _tokenizer: SpelTokenizer | null = null;
  let _tokens: Array<{ from: number; to: number; style: string }> = [];
  let _tokenIndex = 0;
 
  const startState = (): null => null;
 
  const token = (stream: StringStream): string | null => {
    // Tokenize entire input on first call
    if (!_tokenizer || _tokenIndex === 0) {
      _tokenizer = new SpelTokenizer(stream.string);
      let rawTokens: ReturnType<SpelTokenizer['tokenize']>;
      try {
        rawTokens = _tokenizer.tokenize();
      } catch {
        // Tokenizer threw on invalid input (e.g. unterminated string) —
        // treat entire content as unhighlighted plain text
        _tokens = [];
        _tokenIndex = 0;
        stream.skipToEnd();
        return null;
      }
      _tokens = [];
      _tokenIndex = 0;
 
      for (let i = 0; i < rawTokens.length - 1; i++) {
        // Skip EOF
        const tok = rawTokens[i]!;
        const style = tokenKindToStyle(tok.kind);
 
        // Handle special cases
        if (tok.kind === TokenKind.IDENTIFIER) {
          const prevTok = i > 0 ? rawTokens[i - 1] : null;
          if (prevTok?.kind === TokenKind.HASH) {
            // #variableName — highlight the variable name
            _tokens.push({
              from: tok.startPos,
              to: tok.endPos,
              style: 'variableName',
            });
            continue;
          }
          if (prevTok?.kind === TokenKind.DOT || prevTok?.kind === TokenKind.SAFE_NAV) {
            // .property or ?.property
            _tokens.push({
              from: tok.startPos,
              to: tok.endPos,
              style: 'propertyName',
            });
            continue;
          }
        }
 
        _tokens.push({ from: tok.startPos, to: tok.endPos, style });
      }
    }
 
    // Return tokens in order
    while (_tokenIndex < _tokens.length) {
      const t = _tokens[_tokenIndex]!;
      if (t.from >= stream.pos) {
        stream.pos = t.to;
        _tokenIndex++;
        return t.style || null;
      }
      _tokenIndex++;
    }
 
    // Skip to end
    stream.skipToEnd();
    _tokenIndex = 0; // Reset for next parse
    return null;
  };
 
  return { startState, token };
}