Language Service¶
Audience: Developers building IDE integrations or code editors with expr-eval support.
The library includes a built-in language service that provides IDE-like features for expr-eval expressions. This is useful for integrating expr-eval into code editors like Monaco Editor (used by VS Code).
Features¶
- Code Completions - Autocomplete for functions, operators, keywords, and user-defined variables
- Snippet support - Function completions include tab stops with parameter placeholders (e.g.,
sum(${1:a})) - Path-based variable completions - Completions for nested object properties (e.g., typing
user.showsuser.name,user.profile.email) - Text edits with ranges - Proper replacement ranges for more accurate completions
- Hover Information - Documentation tooltips when hovering over functions and variables
- Variable value previews - Hovers on variables show a truncated JSON preview of the value
- Nested path support - Hovering over
user.nameresolves and shows the value at that path - Syntax Highlighting - Token-based highlighting for numbers, strings, keywords, operators, etc.
- Diagnostics - Error detection for function argument count validation
- Too few arguments - Reports when a function is called with fewer arguments than required (e.g.,
pow(2)needs 2 arguments) - Too many arguments - Reports when a function is called with more arguments than allowed (e.g.,
random(1, 2, 3)accepts at most 1) - Variadic functions - Correctly handles functions that accept unlimited arguments (e.g.,
min,max,coalesce)
Basic Usage¶
import { createLanguageService } from '@pro-fa/expr-eval';
const ls = createLanguageService();
// Define variables available in your expressions
const variables = { x: 42, user: { name: 'Ada' }, flag: true };
// Get completions at a position
const completions = ls.getCompletions({
textDocument: doc, // LSP-compatible text document
position: { line: 0, character: 5 },
variables
});
// Get hover information
const hover = ls.getHover({
textDocument: doc,
position: { line: 0, character: 3 },
variables
});
// Get syntax highlighting tokens
const tokens = ls.getHighlighting(doc);
// Get diagnostics (function argument count errors)
const diagnostics = ls.getDiagnostics({ textDocument: doc });
Monaco Editor Integration Sample¶
A complete working example of Monaco Editor integration is included in the repository. To run it:
Then open http://localhost:8080 in your browser. The sample demonstrates:
- Autocompletion for built-in functions (
sum,max,min, etc.) and user variables - Hover documentation for functions and variables
- Live syntax highlighting
- Real-time expression evaluation
- Diagnostics - Red squiggly underlines for function argument count errors (select the "Diagnostics Demo" example to see this in action)
The sample code is located in samples/language-service-sample/ and shows how to:
- Register a custom language with Monaco
- Connect the language service to Monaco's completion and hover providers
- Apply syntax highlighting using decorations
- Create an LSP-compatible text document wrapper for Monaco models
- Display diagnostics using Monaco's
setModelMarkersAPI
Advanced Features¶
Nested Variable Completions¶
The language service supports path-based completions for nested object properties. When you type a dot after a variable name, you'll get completions for its properties:
const variables = {
user: {
name: 'Ada',
profile: {
email: 'ada@example.com',
age: 30
}
},
config: {
timeout: 5000,
retries: 3
}
};
// Typing "user." will show completions: user.name, user.profile
// Typing "user.profile." will show: user.profile.email, user.profile.age
Monaco Editor Integration: Add triggerCharacters: ['.'] to your completion provider to automatically trigger completions when typing a dot:
monaco.languages.registerCompletionItemProvider(languageId, {
triggerCharacters: ['.'],
provideCompletionItems: function (model, position) {
// ... completion logic
}
});
Snippet Support in Completions¶
Function completions include snippet support with tab stops for parameters. This provides a better editing experience in editors that support snippets:
// When completing a function like "sum", the insertText is "sum(${1:a})"
// After selecting the completion:
// 1. The text "sum(a)" is inserted
// 2. The parameter "a" is selected, ready for editing
// 3. You can tab to the next parameter (if any)
Monaco Editor Integration: Use insertTextRules with monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet when the completion's insertTextFormat is 2 (snippet):
const suggestions = items.map(it => ({
label: it.label,
kind: mapKind(it.kind),
detail: it.detail,
documentation: it.documentation,
insertText: it.insertText || it.label,
insertTextRules: it.insertTextFormat === 2
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
: undefined,
range
}));
Text Edit Ranges¶
Completion items may include a textEdit with a specific range for more precise text replacement. This is especially important for path-based completions where only the partial segment after the last dot should be replaced:
// When completing "user.na|" (cursor at |), only "na" should be replaced, not "user.na"
// The textEdit.range will specify the exact range to replace
Monaco Editor Integration: Check for textEdit.range and use it when available:
const suggestions = items.map(it => {
const range = it.textEdit?.range
? new monaco.Range(
it.textEdit.range.start.line + 1,
it.textEdit.range.start.character + 1,
it.textEdit.range.end.line + 1,
it.textEdit.range.end.character + 1
)
: defaultRange;
return {
label: it.label,
insertText: it.textEdit?.newText || it.insertText || it.label,
range
};
});
Variable Value Previews in Hover¶
When hovering over a variable (including nested paths), the hover will display: - The variable's type - A truncated JSON preview of its value
const variables = {
user: { name: 'Ada', score: 95 },
items: [1, 2, 3, 4, 5]
};
// Hovering over "user" shows:
// user: Variable (object)
// Value Preview
// {
// "name": "Ada",
// "score": 95
// }
// Hovering over "user.name" shows:
// user.name: Variable (string)
// Value Preview
// "Ada"
The preview is automatically truncated to prevent overwhelming hovers with large data structures.
HoverV2 Type¶
The getHover method returns a HoverV2 type, which guarantees that contents is a MarkupContent object (not a deprecated string or array):
interface HoverV2 extends Hover {
contents: MarkupContent; // Always MarkupContent, never string or array
}
Monaco Editor Integration: The hover contents are always in the MarkupContent format:
const hover = ls.getHover({textDocument: doc, position, variables});
if (hover && hover.contents) {
// hover.contents is always a MarkupContent object
const value = hover.contents.value;
const kind = hover.contents.kind; // 'plaintext' or 'markdown'
contents = [{value}];
}
API Reference¶
createLanguageService(options?)¶
Creates a new language service instance.
Parameters:
- options (optional): LanguageServiceOptions - Configuration options for the language service
- operators: Record<string, boolean> - Map of operator names to booleans indicating whether they are allowed
Returns: LanguageServiceApi - The language service instance
Example:
import { createLanguageService } from '@pro-fa/expr-eval';
const ls = createLanguageService({
operators: {
'+': true,
'-': true,
'*': true,
'/': true
}
});
ls.getCompletions(params)¶
Returns a list of possible completions for the given position in the document.
Parameters:
- params: GetCompletionsParams
- textDocument: TextDocument - The text document to analyze
- position: Position - The cursor position (0-based line and character)
- variables: Values (optional) - User-defined variables available in the expression
Returns: CompletionItem[] - Array of completion items
CompletionItem Properties:
- label: string - The display label
- kind: CompletionItemKind - The kind of completion (Function, Variable, Keyword, etc.)
- detail: string (optional) - Additional details shown in the completion UI
- documentation: string | MarkupContent (optional) - Documentation for the item
- insertText: string (optional) - The text to insert (may be a snippet)
- insertTextFormat: InsertTextFormat (optional) - 1 = PlainText, 2 = Snippet
- textEdit: TextEdit (optional) - Text edit with specific range and newText
Example:
const completions = ls.getCompletions({
textDocument: doc,
position: { line: 0, character: 5 },
variables: { user: { name: 'Ada' }, x: 42 }
});
ls.getHover(params)¶
Returns hover information for the given position in the document.
Parameters:
- params: GetHoverParams
- textDocument: TextDocument - The text document to analyze
- position: Position - The cursor position (0-based line and character)
- variables: Values (optional) - User-defined variables available in the expression
Returns: HoverV2 - Hover information with guaranteed MarkupContent
HoverV2 Properties:
- contents: MarkupContent - The hover content
- kind: MarkupKind - Either 'plaintext' or 'markdown'
- value: string - The hover text
- range: Range (optional) - The range of the hovered element
Example:
const hover = ls.getHover({
textDocument: doc,
position: { line: 0, character: 3 },
variables: { user: { name: 'Ada' } }
});
console.log(hover.contents.value); // The hover text
console.log(hover.contents.kind); // 'markdown' or 'plaintext'
ls.getHighlighting(textDocument)¶
Returns a list of syntax highlighting tokens for the given text document.
Parameters:
- textDocument: TextDocument - The text document to analyze
Returns: HighlightToken[] - Array of highlighting tokens
HighlightToken Properties:
- type: 'number' | 'string' | 'name' | 'keyword' | 'operator' | 'function' | 'punctuation'
- start: number - Start offset in the document
- end: number - End offset in the document
- value: string | number | boolean | undefined (optional) - The token value
Example:
const tokens = ls.getHighlighting(doc);
tokens.forEach(token => {
console.log(`${token.type} at ${token.start}-${token.end}: ${token.value}`);
});
ls.getDiagnostics(params)¶
Returns a list of diagnostics for the given text document. Currently validates function argument counts.
Parameters:
- params: GetDiagnosticsParams
- textDocument: TextDocument - The text document to analyze
Returns: Diagnostic[] - Array of LSP-compatible diagnostic objects
Diagnostic Properties:
- range: Range - The range of the problematic function call
- severity: DiagnosticSeverity - The severity level (Error)
- message: string - Human-readable description of the issue
- source: string - Always 'expr-eval'
Example:
const diagnostics = ls.getDiagnostics({ textDocument: doc });
diagnostics.forEach(d => {
console.log(`${d.message} at line ${d.range.start.line}`);
});
// For expression "pow(2) + random(1, 2, 3)":
// "Function 'pow' expects at least 2 arguments, but got 1." at line 0
// "Function 'random' expects at most 1 argument, but got 3." at line 0
Monaco Editor Integration:
function applyDiagnostics() {
const doc = makeTextDocument(model);
const diagnostics = ls.getDiagnostics({ textDocument: doc });
const markers = diagnostics.map(d => ({
severity: monaco.MarkerSeverity.Error,
message: d.message,
startLineNumber: d.range.start.line + 1,
startColumn: d.range.start.character + 1,
endLineNumber: d.range.end.line + 1,
endColumn: d.range.end.character + 1,
source: d.source
}));
monaco.editor.setModelMarkers(model, 'expr-eval', markers);
}
TypeScript Types¶
The library exports the following TypeScript types for use in your applications:
Exported Types¶
import type {
LanguageServiceApi,
HoverV2,
GetCompletionsParams,
GetHoverParams,
GetDiagnosticsParams,
HighlightToken,
LanguageServiceOptions,
ArityInfo
} from '@pro-fa/expr-eval';
LanguageServiceApi- The main language service interface withgetCompletions,getHover,getHighlighting, andgetDiagnosticsmethodsHoverV2- Extended Hover type with guaranteedMarkupContentfor contents (not deprecated string/array formats)GetCompletionsParams- Parameters forgetCompletions:textDocument,position, and optionalvariablesGetHoverParams- Parameters forgetHover:textDocument,position, and optionalvariablesGetDiagnosticsParams- Parameters forgetDiagnostics:textDocumentHighlightToken- Syntax highlighting token withtype,start,end, and optionalvalueLanguageServiceOptions- Configuration options for creating a language service, including optionaloperatorsmapArityInfo- Describes a function's expected argument count withminand optionalmax(undefined for variadic functions)
LSP Types¶
The language service uses types from vscode-languageserver-types for LSP compatibility:
import type {
Position,
Range,
CompletionItem,
CompletionItemKind,
MarkupContent,
MarkupKind,
InsertTextFormat,
Diagnostic,
DiagnosticSeverity
} from 'vscode-languageserver-types';
import type { TextDocument } from 'vscode-languageserver-textdocument';
These types ensure compatibility with Language Server Protocol-based editors and tools.