UNPKG

jexl-extended

Version:

Extended grammar for Javascript Expression Language (JEXL)

223 lines (222 loc) 10.6 kB
/** * Monaco Editor Language Registration for JEXL * Helper functions to register JEXL language support with Monaco Editor */ import { jexlLanguageConfiguration } from './language-configuration'; import { jexlMonarchLanguage } from './monarch-language'; import { createJexlCompletionItems, createJexlKeywords, createJexlOperators, getJexlCompletionDoc, getOperatorDoc, } from "./completion-provider"; export const JEXL_LANGUAGE_ID = "jexl"; /** * Registers JEXL language support with Monaco Editor * @param monaco - The Monaco Editor instance */ export 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 === JEXL_LANGUAGE_ID); if (isAlreadyRegistered) { return; } // Register the language monaco.languages.register({ id: JEXL_LANGUAGE_ID, extensions: [".jexl"], aliases: ["JEXL", "jexl"], mimetypes: ["text/jexl"], }); // Set language configuration monaco.languages.setLanguageConfiguration(JEXL_LANGUAGE_ID, jexlLanguageConfiguration); // Set monarch tokenizer monaco.languages.setMonarchTokensProvider(JEXL_LANGUAGE_ID, jexlMonarchLanguage); // Register completion provider monaco.languages.registerCompletionItemProvider(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, }); // The current word being typed (for filtering) const currentWord = word.word || ""; // 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, filter by current word const transformItems = createJexlCompletionItems("transform", currentWord); return { suggestions: transformItems.map((item) => ({ ...item, range })), }; } else if (isAfterIdentifier) { // After identifier: suggest operators and pipe const operatorItems = createJexlOperators(); return { suggestions: operatorItems.map((item) => ({ ...item, range })), }; } else if (isAfterOperatorOrStart) { // At start or after operator: show functions and keywords, filter by current word const functionItems = createJexlCompletionItems("function", currentWord); const keywordItems = createJexlKeywords(); return { suggestions: [ ...functionItems.map((item) => ({ ...item, range })), ...keywordItems.map((item) => ({ ...item, range })), ], }; } else { // Fallback: show all functions and keywords, filter by current word const functionItems = createJexlCompletionItems(undefined, currentWord); const keywordItems = createJexlKeywords(); return { suggestions: [ ...functionItems.map((item) => ({ ...item, range })), ...keywordItems.map((item) => ({ ...item, range })), ], }; } }, }); // Register hover provider monaco.languages.registerHoverProvider(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 = 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 = 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(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 */ export function isJexlLanguageRegistered(monaco) { const registeredLanguages = monaco.languages.getLanguages(); return registeredLanguages.some((lang) => lang.id === 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 */ export function createJexlEditor(monaco, container, options = {}) { // Ensure JEXL language is registered (safe to call multiple times) registerJexlLanguage(monaco); return monaco.editor.create(container, { language: JEXL_LANGUAGE_ID, theme: 'vs-dark', automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: 14, lineNumbers: 'on', folding: true, ...options }); }