import { createConnection, TextDocuments, Diagnostic, DiagnosticSeverity, ProposedFeatures, InitializeParams, InitializeResult, TextDocumentSyncKind, Location, Range, Position, DefinitionParams } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { EftlTokenizer, TokenType } from './tokenizer'; import { EftlParser } from './parser'; // Create a connection for the server const connection = createConnection(ProposedFeatures.all); // Create a document manager const documents: TextDocuments = new TextDocuments(TextDocument); // Store variable definitions per document interface VariableDefinition { name: string; line: number; column: number; length: number; } const documentVariables: Map = new Map(); connection.onInitialize((params: InitializeParams): InitializeResult => { return { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, definitionProvider: true } }; }); // Handle "Go to Definition" requests connection.onDefinition((params: DefinitionParams): Location | null => { const document = documents.get(params.textDocument.uri); if (!document) { return null; } const text = document.getText(); const offset = document.offsetAt(params.position); // Find the word at the current position const wordRange = getWordRangeAtPosition(text, offset); if (!wordRange) { return null; } const word = text.substring(wordRange.start, wordRange.end); // Look for variable definition const variables = documentVariables.get(params.textDocument.uri) || []; const variable = variables.find(v => v.name === word); if (variable) { return { uri: params.textDocument.uri, range: { start: { line: variable.line - 1, character: variable.column - 1 }, end: { line: variable.line - 1, character: variable.column - 1 + variable.length } } }; } return null; }); function getWordRangeAtPosition(text: string, offset: number): { start: number; end: number } | null { // Find word boundaries (alphanumeric and underscore) let start = offset; let end = offset; // Move start backwards while (start > 0 && /[a-zA-Z0-9_]/.test(text[start - 1])) { start--; } // Move end forwards while (end < text.length && /[a-zA-Z0-9_]/.test(text[end])) { end++; } if (start === end) { return null; } return { start, end }; } // Validate documents when they change documents.onDidChangeContent(change => { validateDocument(change.document); }); async function validateDocument(textDocument: TextDocument): Promise { const text = textDocument.getText(); const diagnostics: Diagnostic[] = []; // Tokenize const tokenizer = new EftlTokenizer(text); const { tokens, errors: tokenErrors } = tokenizer.tokenize(); // Valid types for VAR const validTypes = ['string', 'boolean', 'number', 'date', 'object', 'iterable']; // Extract variable definitions and validate types const variables: VariableDefinition[] = []; for (const token of tokens) { if (token.type === TokenType.VAR_OPEN) { // Parse name="varName" from the token value const nameMatch = token.value.match(/name="([^"]+)"/); if (nameMatch) { variables.push({ name: nameMatch[1], line: token.line, column: token.column, length: token.length }); } // Validate type attribute const typeMatch = token.value.match(/type="([^"]*)"/); if (typeMatch) { const typeValue = typeMatch[1]; if (!validTypes.includes(typeValue)) { // Find position of the type value in the token const typeIndex = token.value.indexOf(`type="${typeValue}"`); const typeValueStart = typeIndex + 6; // 'type="'.length diagnostics.push({ severity: DiagnosticSeverity.Error, range: { start: { line: token.line - 1, character: token.column - 1 + typeValueStart }, end: { line: token.line - 1, character: token.column - 1 + typeValueStart + typeValue.length } }, message: `Invalid type "${typeValue}". Must be one of: ${validTypes.join(', ')}`, source: 'eftl' }); } } } } documentVariables.set(textDocument.uri, variables); // Add tokenizer errors for (const error of tokenErrors) { diagnostics.push({ severity: DiagnosticSeverity.Error, range: { start: { line: error.line - 1, character: error.column - 1 }, end: { line: error.line - 1, character: error.column - 1 + error.length } }, message: error.message, source: 'eftl' }); } // Parse const parser = new EftlParser(tokens); const { errors: parseErrors } = parser.parse(); // Add parser errors for (const error of parseErrors) { diagnostics.push({ severity: DiagnosticSeverity.Error, range: { start: { line: error.line - 1, character: error.column - 1 }, end: { line: error.line - 1, character: error.column - 1 + error.length } }, message: error.message, source: 'eftl' }); } // Send diagnostics to VS Code connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); } // Clean up when documents are closed documents.onDidClose(e => { documentVariables.delete(e.document.uri); }); // Make the text document manager listen on the connection documents.listen(connection); // Listen on the connection connection.listen();