Skip to content

LanguageService

Defined in: packages/engine/src/language/LanguageService.ts:155

Editor-agnostic “language server” for solve expressions: turns a line of text into semantic token ranges, using the exact same lexer real evaluation uses (so it only ever classifies what the engine’s grammar actually recognizes, never a separate/duplicated tokenizer). No knowledge of CSS, CodeMirror, VS Code, or any other rendering concept lives here. See language/adapters/ for that.

Classification happens at the LEXER stage, before the normalizer runs (normalization, phrase fusion, implicit multiply, and package-specific rules, happens later, only on the real evaluation path). A package’s lexer-level custom token types (e.g. a custom keyword) are recognized here exactly as evaluation would see them. A package’s normalizer-fused synthetic tokens (e.g. OSRS’s GAME_ITEM, built by fusing several consecutive IDENT tokens against an item-name trie) are NOT. This service still shows the pre-fusion IDENT tokens individually for those. IEnginePackage.tokenCategories entries for normalizer-only token types are still valid, correct registrations (queryable via getTokenCategory) , they just won’t currently be reachable through this lexer-only classification path. Folding normalization in would require running it per keystroke on the highlighting path too, which needs its own careful design (span recomputation for fused multi-token ranges, in particular) rather than a quick addition here.

Lexing alone is NOT sufficient to decide “recognized”, though: a run of plain-English words (“My name is ron”) lexes into a sequence of individually-valid IDENT tokens with no grammar tying them together every word “recognized” at the token level, but the line as a whole is not something the engine would ever accept as an expression. Surfacing per-token colors for that case looks like the editor mistook prose for code. So a line’s tokens are only surfaced once the line as a whole parses successfully (via ExpressionEngine.compileExpression, the same parse pipeline, and the same bytecode cache, real evaluation uses; no separate/duplicated grammar check). A single bare word (“hello”, a valid variable reference) or a keyword-only line (“pi”) still parses and still highlights, only genuinely ungrammatical text is suppressed, unless it’s a known variable elsewhere in the document (see variableNameSource).

getCompletions() is the other half of this “language server”: unlike getSemanticTokens(), it’s explicitly for incomplete, mid-typing text , it deliberately does NOT gate on parse validity (a half-typed expression almost never parses), using simple prefix matching instead.

Must be constructed with an already-configured ExpressionEngine (one with all currently-relevant packages registered) rather than a bare lexer, reusing an existing engine is both the fast path (no throwaway lexer construction) and the correct one: a highlighting-only lexer built independently of the evaluation engine would silently fail to recognize plugin-contributed tokens (e.g. a package’s custom keywords) unless it happened to have the identical packages registered.

new LanguageService(engine?, options?): LanguageService;

Defined in: packages/engine/src/language/LanguageService.ts:197

ParameterType
engine?| ExpressionEngine | null
options?LanguageServiceOptions

LanguageService

getCompletions(lineText, cursorOffset): CompletionItem[];

Defined in: packages/engine/src/language/LanguageService.ts:409

Completion candidates for the identifier prefix immediately before cursorOffset on lineText. Deliberately simple prefix matching, not parser-driven “what’s grammatically valid here” prediction, a half-typed expression almost never parses, so gating on parse validity (the way getSemanticTokens does) would suppress completions almost always. This is the safest, fastest option that still delivers real value.

Candidates come from three sources: keywords (which already include function names. See ExpressionLexer.getKeywords()’s doc comment) and units, both static per engine configuration and cached lazily; package-contributed items (IEnginePackage.completionItems), same cache; and variable names, read fresh from variableNameSource() on every call since those change on every edit.

ParameterType
lineTextstring
cursorOffsetnumber

CompletionItem[]


getSemanticTokens(lineText, lineNumber): SemanticToken[];

Defined in: packages/engine/src/language/LanguageService.ts:309

Classify every recognized token on one line.

ParameterTypeDescription
lineTextstringThe raw line text (may be a markdown-structural line the engine’s classifier skips, that’s handled by the underlying lexer, which returns no tokens for those).
lineNumbernumber1-based line number, used purely as a cache key.

SemanticToken[]


invalidateCache(): void;

Defined in: packages/engine/src/language/LanguageService.ts:546

Full cache clear. Reserved for cases with no meaningful “which lines changed” (e.g. the document was swapped wholesale, or a package was registered/unregistered mid-session, changing what categories exist). Prefer invalidateLines for ordinary edits. Also rebuilds the lazily-cached keyword/unit/package-item completion candidates on next use, the only thing that can change that list mid-session.

void


invalidateLines(lineNumbers): void;

Defined in: packages/engine/src/language/LanguageService.ts:532

Evict specific lines (e.g. the lines actually touched by a CodeMirror change set) instead of the whole cache, the surgical counterpart to invalidateCache, letting a single-line edit stay cheap even in a large document: every other cached line is untouched and still hits on the next call.

ParameterType
lineNumbersIterable<number>

void