200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
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();
|