eo-lsp-server
Version:
Language Server for a syntax highlighter for the EO Language
235 lines (214 loc) • 7.26 kB
text/typescript
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 Objectionary.com
// SPDX-License-Identifier: MIT
import {
createConnection,
TextDocuments,
Diagnostic,
DiagnosticSeverity,
ProposedFeatures,
InitializeParams,
DidChangeConfigurationNotification,
TextDocumentSyncKind,
InitializeResult,
SemanticTokensRegistrationOptions,
SemanticTokensRegistrationType
} from "vscode-languageserver/node.js";
import { TextDocument } from "vscode-languageserver-textdocument";
import { Capabilities } from "./capabilities";
import { EoVersion } from "./eo-version";
import { SemanticTokensProvider } from "./semantics";
import { getParserErrors } from "./parser";
import { ParserError } from "./parserError";
import { DefaultSettings } from "./defaultSettings";
/**
* Connection with the server, using Node's IPC as a transport.
* Also includes all preview / proposed LSP features.
*/
const connection = createConnection(ProposedFeatures.all);
/**
* Simple text document manager.
*/
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
/**
* Client capabilities manager, to define what is and is not able to do.
*/
const capabilities = new Capabilities();
/**
* Provider of the semantic highlighting capability of the language server.
*/
let provider: SemanticTokensProvider;
/**
* Defines procedures to be executed on the initialization process
* of the connection with the client
*/
connection.onInitialize((params: InitializeParams) => {
const caps = params.capabilities;
capabilities.initialize(caps);
provider = new SemanticTokensProvider(params.capabilities.textDocument!.semanticTokens!);
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental
}
};
if (capabilities.workspace) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true
}
};
}
return result;
});
/**
* Defines procedures to be executed once initialization process
* of the connection with the client has concluded.
*
* Registers the following possible capabilities of the client:
* Configuration, Workspace Folder and Document Semantic Tokens
*/
connection.onInitialized(() => {
if (capabilities.configuration) {
connection.client.register(DidChangeConfigurationNotification.type, void 0);
}
if (capabilities.workspace) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log("Workspace folder change event received");
});
}
if (capabilities.tokens) {
const options: SemanticTokensRegistrationOptions = {
documentSelector: null,
legend: provider.legend,
range: false,
full: {
delta: true
}
};
connection.client.register(SemanticTokensRegistrationType.type, options);
}
});
/**
* Settings of the Language Server
*/
const defaultSettings: DefaultSettings = { limit: 1000 };
/**
* The global settings, used when the `workspace/configuration` request is not supported by the client.
*/
let settings: DefaultSettings = defaultSettings;
/**
* Cache for the settings of all open documents
*/
const cache: Map<string, Thenable<DefaultSettings>> = new Map();
/**
* Retrieves the settings for a document
* @param resource - String for the scheme of the document for which to retrieve its settings
* @returns - A Promise for the settings of the document requested
*/
function getDocumentSettings(resource: string): Thenable<DefaultSettings> {
if (!capabilities.configuration) {
return Promise.resolve(settings);
}
let result = cache.get(resource);
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: "languageServerExample"
}).then(config => ((config && typeof config === "object") ? config : defaultSettings));
cache.set(resource, result);
}
return result;
}
/**
* Performs error checking for the given document through its parsing. Sends to VSCode
* each problem returned by the parser up until the maximum number of problems defined
* in the given document's settings.
* @param document - Document for which to perform the validation procedure
* @returns {Promise<void>}
*/
async function validateTextDocument(document: TextDocument): Promise<void> {
const config = await getDocumentSettings(document.uri);
const text = document.getText();
const diagnostics: Diagnostic[] = [];
const errors = getParserErrors(text);
const effective = config || defaultSettings;
const limit = effective.limit;
errors.forEach((error, index) => {
if (limit !== null && index >= limit) {
return;
}
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: { line: error.line - 1, character: error.column },
end: { line: error.line - 1, character: error.column }
},
message: `${error.msg} (EO ${EoVersion})`,
source: "ex"
};
diagnostics.push(diagnostic);
});
connection.sendDiagnostics({ uri: document.uri, diagnostics });
}
/**
* Resets all cached document settings and revalidates all open text
* documents with there is a change in the configuration of the client.
*/
connection.onDidChangeConfiguration(change => {
if (capabilities.configuration) {
cache.clear();
} else {
const config = change.settings.languageServerExample;
settings = (config && typeof config === "object") ? config : defaultSettings;
}
documents.all().forEach(validateTextDocument);
});
/**
* Clears the settings cache for a closed document, once it is closed
*/
documents.onDidClose(e => {
cache.delete(e.document.uri);
});
/**
* Performs the validation of a document once it is opened or its content is
* modified.
*/
documents.onDidChangeContent(change => {
validateTextDocument(change.document);
});
/**
* Logs if a change in a watched document is detected
*/
connection.onDidChangeWatchedFiles(_change => {
connection.console.log("We received a file change event");
});
/**
* Performs semantic highlighting for the document defined in the
* callback parameter once the document is first opened.
*/
connection.languages.semanticTokens.on(params => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return { data: [] };
}
return provider.provideSemanticTokens(document);
});
/**
* Performs semantic highlighting for the document defined in the
* callback parameter once the document is changed.
*/
connection.languages.semanticTokens.onDelta(params => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return { data: [] };
}
return provider.provideDeltas(document);
});
/**
* Make the text document manager listen on the connection
* for open, change and close text document events
*/
documents.listen(connection);
/**
* Listen on the connection
*/
connection.listen();