add first working version

This commit is contained in:
2026-08-17 18:22:16 +02:00
parent 160ed5e409
commit f783617393
12 changed files with 1927 additions and 2 deletions
+199
View File
@@ -0,0 +1,199 @@
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<TextDocument> = new TextDocuments(TextDocument);
// Store variable definitions per document
interface VariableDefinition {
name: string;
line: number;
column: number;
length: number;
}
const documentVariables: Map<string, VariableDefinition[]> = 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<void> {
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();