jexl-extended
Version:
Extended grammar for Javascript Expression Language (JEXL)
226 lines (225 loc) • 10.7 kB
JavaScript
"use strict";
/**
* Monaco Editor Language Registration for JEXL
* Helper functions to register JEXL language support with Monaco Editor
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.JEXL_LANGUAGE_ID = void 0;
exports.registerJexlLanguage = registerJexlLanguage;
exports.isJexlLanguageRegistered = isJexlLanguageRegistered;
exports.createJexlEditor = createJexlEditor;
const language_configuration_1 = require("./language-configuration");
const monarch_language_1 = require("./monarch-language");
const completion_provider_1 = require("./completion-provider");
exports.JEXL_LANGUAGE_ID = 'jexl';
/**
* Registers JEXL language support with Monaco Editor
* @param monaco - The Monaco Editor instance
*/
function registerJexlLanguage(monaco) {
// Check if language is already registered using Monaco's API
const registeredLanguages = monaco.languages.getLanguages();
const isAlreadyRegistered = registeredLanguages.some((lang) => lang.id === exports.JEXL_LANGUAGE_ID);
if (isAlreadyRegistered) {
return;
}
// Register the language
monaco.languages.register({
id: exports.JEXL_LANGUAGE_ID,
extensions: ['.jexl'],
aliases: ['JEXL', 'jexl'],
mimetypes: ['text/jexl']
});
// Set language configuration
monaco.languages.setLanguageConfiguration(exports.JEXL_LANGUAGE_ID, language_configuration_1.jexlLanguageConfiguration);
// Set monarch tokenizer
monaco.languages.setMonarchTokensProvider(exports.JEXL_LANGUAGE_ID, monarch_language_1.jexlMonarchLanguage);
// Register completion provider
monaco.languages.registerCompletionItemProvider(exports.JEXL_LANGUAGE_ID, {
provideCompletionItems: function (model, position) {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
// Get text before cursor to analyze context
const textBeforeCursor = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
// Look for the last pipe operator before the cursor
const lastPipeIndex = textBeforeCursor.lastIndexOf('|');
const isAfterPipe = lastPipeIndex !== -1 &&
// Make sure there's no significant content after the pipe (just whitespace and partial word)
/^\s*\w*$/.test(textBeforeCursor.substring(lastPipeIndex + 1));
// Check if we're after an identifier (function, variable, etc.)
// This regex matches: word characters, closing brackets/parens, or numbers at the end
const afterIdentifierMatch = textBeforeCursor.match(/[\w\]\)]\s*$/);
const isAfterIdentifier = afterIdentifierMatch !== null && !isAfterPipe;
// Check if we're at the start or after an operator
const afterOperatorMatch = textBeforeCursor.match(/[+\-*/%^=!<>&|?:,(\[\s]\s*$/);
const isAfterOperatorOrStart = afterOperatorMatch !== null || textBeforeCursor.trim() === '';
if (isAfterPipe) {
// After pipe: only show transforms
const transformItems = (0, completion_provider_1.createJexlTransformItems)();
return {
suggestions: transformItems.map(item => ({ ...item, range }))
};
}
else if (isAfterIdentifier) {
// After identifier: suggest operators and pipe
const operatorItems = (0, completion_provider_1.createJexlOperators)();
return {
suggestions: operatorItems.map(item => ({ ...item, range }))
};
}
else if (isAfterOperatorOrStart) {
// At start or after operator: show functions and keywords
const functionItems = (0, completion_provider_1.createJexlFunctionItems)();
const keywordItems = (0, completion_provider_1.createJexlKeywords)();
return {
suggestions: [
...functionItems.map(item => ({ ...item, range })),
...keywordItems.map(item => ({ ...item, range }))
]
};
}
else {
// Fallback: show all functions and keywords
const functionItems = (0, completion_provider_1.createJexlFunctionItems)();
const keywordItems = (0, completion_provider_1.createJexlKeywords)();
return {
suggestions: [
...functionItems.map(item => ({ ...item, range })),
...keywordItems.map(item => ({ ...item, range }))
]
};
}
}
});
// Register hover provider
monaco.languages.registerHoverProvider(exports.JEXL_LANGUAGE_ID, {
provideHover: function (model, position) {
const word = model.getWordAtPosition(position);
if (!word)
return;
// Check if we're after a pipe operator to determine context
const textBeforeCursor = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: word.startColumn
});
// Look for the last pipe operator before this word
const lastPipeIndex = textBeforeCursor.lastIndexOf('|');
const isAfterPipe = lastPipeIndex !== -1 &&
// Make sure there's no significant content after the pipe (just whitespace)
/^\s*$/.test(textBeforeCursor.substring(lastPipeIndex + 1));
// Determine preferred type based on context
const preferredType = isAfterPipe ? 'transform' : 'function';
// Try to get documentation for function/transform
const doc = (0, completion_provider_1.getJexlCompletionDoc)(word.word, preferredType);
if (doc) {
// Create formatted markdown content
const markdownContent = `**${doc.label}** (${doc.type}) - ${doc.detail}\n\n${doc.documentation}`;
return {
range: new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
contents: [
{ value: markdownContent }
]
};
}
// If no function/transform found, try operators
const operatorDoc = (0, completion_provider_1.getOperatorDoc)(word.word);
if (operatorDoc) {
return {
range: new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
contents: [
{ value: `**${operatorDoc.label}** - ${operatorDoc.detail}\n\n${operatorDoc.documentation}` }
]
};
}
}
});
// Register folding provider for arrays and objects
monaco.languages.registerFoldingRangeProvider(exports.JEXL_LANGUAGE_ID, {
provideFoldingRanges: function (model) {
const ranges = [];
const text = model.getValue();
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Find opening brackets/braces
for (let j = 0; j < line.length; j++) {
if (line[j] === '[' || line[j] === '{') {
const openChar = line[j];
const closeChar = openChar === '[' ? ']' : '}';
// Find matching closing bracket/brace
let depth = 1;
let found = false;
for (let k = i; k < lines.length && !found; k++) {
const startPos = k === i ? j + 1 : 0;
const searchLine = lines[k];
for (let l = startPos; l < searchLine.length; l++) {
if (searchLine[l] === openChar) {
depth++;
}
else if (searchLine[l] === closeChar) {
depth--;
if (depth === 0) {
// Found matching closing bracket
if (k > i) { // Only fold if spans multiple lines
ranges.push({
start: i + 1,
end: k + 1,
kind: monaco.languages.FoldingRangeKind.Region
});
}
found = true;
break;
}
}
}
}
}
}
}
return ranges;
}
});
}
/**
* Checks if JEXL language has been registered
* @param monaco - The Monaco Editor instance
* @returns true if language is already registered
*/
function isJexlLanguageRegistered(monaco) {
const registeredLanguages = monaco.languages.getLanguages();
return registeredLanguages.some((lang) => lang.id === exports.JEXL_LANGUAGE_ID);
}
/**
* Creates a Monaco Editor instance with JEXL language support
* @param monaco - The Monaco Editor instance
* @param container - The DOM element to mount the editor
* @param options - Editor options
* @returns Monaco Editor instance
*/
function createJexlEditor(monaco, container, options = {}) {
// Ensure JEXL language is registered (safe to call multiple times)
registerJexlLanguage(monaco);
return monaco.editor.create(container, {
language: exports.JEXL_LANGUAGE_ID,
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
folding: true,
...options
});
}