pss-langserver
Version:
A Language server for the Portable Stimulus Standard
465 lines (464 loc) • 20.5 kB
JavaScript
;
/*
* Copyright (C) 2025 Darshan(@thisisthedarshan)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_1 = require("vscode-languageserver/node");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
const formattingProvider_1 = require("./providers/formattingProvider");
const builtinFunctions_1 = require("./definitions/builtinFunctions");
const dataTypes_1 = require("./definitions/dataTypes");
const version_1 = require("./version");
const path = __importStar(require("path"));
const url_1 = require("url");
const helpers_1 = require("./helpers");
const helpers_2 = require("./parser/helpers");
const autoCompletionProvider_1 = require("./providers/autoCompletionProvider");
const semanticTokenProvider_1 = require("./providers/semanticTokenProvider");
const gotoProvider_1 = require("./providers/gotoProvider");
const hoverProvider_1 = require("./providers/hoverProvider");
const lodash_debounce_1 = __importDefault(require("lodash.debounce"));
const keywords_1 = require("./definitions/keywords");
const objectCommentsProvider_1 = require("./providers/objectCommentsProvider");
/* To make the process act like an actual executable */
const args = process.argv.slice(2);
if (args.includes('--version') || args.includes('-v')) {
/* Show build information */
(0, version_1.version)();
process.exit(0);
}
/* Support all connection types - ipc, stdio, tcp */
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
/* Documents manager */
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
/* Boolean to track the first scan of files. */
var isFirst = true; /* To check if an ast has already been built or not */
/* Caches */
var fileWiseAST = {}; /* Maybe use it later? */
var pssAST = [];
var autoCompletions = [];
var hoverCache = [];
/* Caches for built-in objects - like keywords and functions */
var hoverCacheBuiltin = [];
var builtInCompletions; /* Holds all autocompletion items */
var semanticTokenCache = undefined;
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = true;
let hasDiagnosticRelatedInformationCapability = false;
/* Setup initialization */
connection.onInitialize((params) => {
const capabilities = params.capabilities;
const workspaceFolders = params.workspaceFolders || [];
const pssFiles = [];
for (const folder of workspaceFolders) {
let dir = decodeURIComponent(folder.uri.replace('file://', ''));
(0, helpers_2.scanDirectory)(dir, pssFiles);
}
/* Process found files */
fileWiseAST = (0, helpers_1.buildASTForFiles)(pssFiles);
pssAST = [];
Object.entries(fileWiseAST).forEach(([uri, ast]) => {
pssAST = [...pssAST, ...ast];
});
if (pssAST.length > 0) {
isFirst = false;
autoCompletions = (0, autoCompletionProvider_1.buildAutocompletions)(pssAST);
hoverCache = (0, hoverProvider_1.buildHoverItems)(pssAST);
semanticTokenCache = (0, semanticTokenProvider_1.generateSemanticTokensAdvanced)(pssAST);
}
/* Does the client support the `workspace/configuration` request? */
/* If not, we fall back using global settings. */
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
hasDiagnosticRelatedInformationCapability = !!(capabilities.textDocument &&
capabilities.textDocument.publishDiagnostics &&
capabilities.textDocument.publishDiagnostics.relatedInformation);
const result = {
capabilities: {
/* Antlr just generates AST (not CST) so incremental sync isn't a good idea */
textDocumentSync: node_1.TextDocumentSyncKind.Full,
/* Tell the client that this server supports code completion. */
completionProvider: {
resolveProvider: true,
triggerCharacters: ['.', ':', ' '],
},
/* Our language has inter-file dependencies */
/* For now, we will not support diagnostics */
diagnosticProvider: undefined,
/*{
interFileDependencies: true,
workspaceDiagnostics: false
},*/
/* Our LSP Supports workspace by default - necessary */
workspace: {
workspaceFolders: {
supported: true
}
},
documentFormattingProvider: true,
signatureHelpProvider: {
triggerCharacters: ['(', ',']
},
semanticTokensProvider: {
legend: dataTypes_1.semanticTokensLegend,
range: false,
full: {
delta: false
},
},
definitionProvider: true,
declarationProvider: true,
referencesProvider: true,
hoverProvider: true,
/* End Capabilities */
}
};
return result;
});
/* Completed initialization */
connection.onInitialized(() => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(node_1.DidChangeConfigurationNotification.type, undefined);
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(_event => {
connection.console.log('Workspace folder change event received.');
});
}
/* Build Caches for hover and auto-completions to speed-up the delivery */
hoverCacheBuiltin = (0, hoverProvider_1.createBuiltinHoverCache)();
builtInCompletions = (0, autoCompletionProvider_1.buildAutocompletionBuiltinsBlock)();
(0, helpers_1.notify)(connection, "PSS Language Server Started :D");
/* Ask the client to get new sematic tokens */
connection.languages.semanticTokens.refresh();
});
/* Default settings - in case config not supported */
const defaultSettings = { tabspaces: 4, fileAuthor: "", formatPatterns: ["=", "//"], autoFormatHeader: false, wrapAt: 0 };
let globalSettings = defaultSettings;
/* Cache the settings of all open documents */
const documentSettings = new Map();
/* When connection/config change */
connection.onDidChangeConfiguration((change) => {
documentSettings.clear();
if (!hasConfigurationCapability) {
globalSettings = change.settings.PSS || defaultSettings;
}
/*connection.languages.diagnostics.refresh();*/
});
/* For future use - with diagnostics */
/*connection.sendDiagnostics({ uri: "", diagnostics: [] });*/
function getSettings(connection, resource) {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings);
}
let result = documentSettings.get(resource);
if (!result) {
result = Promise.all([
connection.workspace.getConfiguration({ scopeUri: resource, section: 'PSS.tabspaces' }),
connection.workspace.getConfiguration({ scopeUri: resource, section: 'PSS.fileAuthor' }),
connection.workspace.getConfiguration({ scopeUri: resource, section: 'PSS.formatPatterns' }),
connection.workspace.getConfiguration({ scopeUri: resource, section: 'PSS.autoFormatHeader' }),
connection.workspace.getConfiguration({ scopeUri: resource, section: 'PSS.wrapAt' })
]).then(([tabspaces, fileAuthor, formatPatterns, autoFormatHeader, wrapAt]) => {
// Validate tabspaces
const validatedTabspaces = Math.min(Math.max(tabspaces ?? defaultSettings.tabspaces, 1), 9);
return {
tabspaces: validatedTabspaces,
fileAuthor: fileAuthor ?? defaultSettings.fileAuthor,
formatPatterns: formatPatterns ?? [],
autoFormatHeader: autoFormatHeader ?? false,
wrapAt: wrapAt < 69 && wrapAt > 0 ? 69 : 0
};
});
documentSettings.set(resource, result);
}
return result;
}
/* File has been closed - so drop that config */
documents.onDidClose(e => {
documentSettings.delete(e.document.uri);
});
connection.onDidOpenTextDocument((params) => {
if (isFirst) {
const file = params.textDocument.uri;
const pssFiles = [];
try {
const filePath = (0, url_1.fileURLToPath)(file);
const folderPath = path.dirname(filePath);
(0, helpers_1.notify)(connection, `Scanning local folder (${folderPath}) for pss files`);
(0, helpers_2.scanDirectory)(folderPath, pssFiles);
fileWiseAST = (0, helpers_1.buildASTForFiles)(pssFiles);
pssAST = [];
Object.entries(fileWiseAST).forEach(([uri, ast]) => {
pssAST = [...pssAST, ...ast];
});
autoCompletions = (0, autoCompletionProvider_1.buildAutocompletions)(pssAST);
hoverCache = (0, hoverProvider_1.buildHoverItems)(pssAST);
semanticTokenCache = (0, semanticTokenProvider_1.generateSemanticTokensAdvanced)(pssAST);
isFirst = false;
// Continue with the rest of your handler...
}
catch (e) {
console.error(`Error parsing URI ${file}: ${e}`);
}
}
});
/* Handle updating AST using debounce */
const debouncedASTBuilder = (0, lodash_debounce_1.default)((uri, content) => {
if (content.length === 0) {
return;
}
(0, helpers_2.updateASTNew)(uri, content).then(result => {
// pssAST = updateASTNewMeta(pssAST, result);
if (result.length > 0) {
fileWiseAST[uri] = result;
pssAST = [];
Object.entries(fileWiseAST).forEach(([uri, ast]) => {
pssAST = [...pssAST, ...ast];
});
autoCompletions = (0, autoCompletionProvider_1.buildAutocompletions)(pssAST);
hoverCache = (0, hoverProvider_1.buildHoverItems)(pssAST);
semanticTokenCache = (0, semanticTokenProvider_1.generateSemanticTokensAdvanced)(pssAST);
}
});
}, 1800);
/* Event when a document is changed or first opened */
documents.onDidChangeContent(change => {
/* Call async file processor */
// updateAST(change.document.uri.toString(), change.document.getText().toString()).then(result => {
// globalAST = updateASTMeta(globalAST, result);
// });
/* New function */
debouncedASTBuilder(change.document.uri.toString(), change.document.getText().toString());
});
/* Refresh semantic tokens on document saves */
connection.onDidSaveTextDocument(save => {
connection.languages.semanticTokens.refresh();
debouncedASTBuilder(save.textDocument.uri, save.text || "");
});
/* See if monitored files have changed */
connection.onDidChangeWatchedFiles(_change => {
connection.console.log('We received a file change event');
});
/* Completions provider */
connection.onCompletion((_textDocumentPosition) => {
// let completions = [...builtInCompletions, ...buildAutocompletionBlock(globalAST)];
// let completions = [...builtInCompletions, ...buildAutocompletionBlockAdvanced(pssAST)]
let completions = [...builtInCompletions, ...autoCompletions];
return [...new Set(completions)];
});
/* Resolve info about selected item in completion list */
connection.onCompletionResolve((item) => {
/* No plans for now - later we will add comment parsing as well */
return item;
});
/* Provide function Signatures */
connection.onSignatureHelp((params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return null;
}
const position = params.position;
const line = document.getText({
start: { line: position.line, character: 0 },
end: position
});
// Updated regex to match function name with parameters
const match = line.match(/(\w+)\((.*)/);
if (!match) {
return null;
}
const funcName = match[1];
// Count commas to determine active parameter
var activeParameter = (match[2].match(/,/g) || []).length;
const refNode = (0, helpers_2.getNodeFromNameArray)(pssAST, funcName);
if (refNode) {
const functionInfo = refNode;
const parameters = functionInfo.parameters.map(p => node_1.ParameterInformation.create(p.paramName, `Function parameter of type ${p.paramType}`));
if (parameters.length > 0 && (parameters[parameters.length - 1].label === "...args" || parameters[parameters.length - 1].label?.toString().includes("...")) && activeParameter > parameters.length - 1) {
activeParameter = parameters.length - 1;
}
const params = functionInfo.parameters
.map(param => `${param.paramType} ${param.paramName}`)
.join(", ");
const document = (typeof functionInfo.comments === 'string') ? functionInfo.comments : (0, helpers_2.buildMarkdownComment)(functionInfo.comments);
const signature = node_1.SignatureInformation.create(`${functionInfo.platformQualifier} function ${functionInfo.returnType} ${functionInfo.name} (${params})`, document, ...parameters);
return {
signatures: [signature],
activeSignature: 0,
activeParameter
};
}
const funcInfo = builtinFunctions_1.builtInSignatures[funcName];
if (!funcInfo) {
return null;
}
const parameters = funcInfo.parameters.map(p => node_1.ParameterInformation.create(p.label, p.documentation));
/* This hack simply ensures that the function signature helper respects varargs... */
if (parameters[parameters.length - 1].label === "...args" && activeParameter > parameters.length - 1) {
activeParameter = parameters.length - 1;
}
const signature = node_1.SignatureInformation.create(funcInfo.signature, funcInfo.documentation + `\nPart of \`${funcInfo.package}\``, ...parameters);
return {
signatures: [signature],
activeSignature: 0,
activeParameter
};
});
/* Provide semantic tokens to the client */
connection.languages.semanticTokens.on((params) => {
const uri = params.textDocument.uri;
const document = documents.get(uri);
if (!document) {
return { data: [] };
}
let semanticTokens = (0, semanticTokenProvider_1.createSemanticTokensFor)(document.getText());
if (semanticTokenCache) {
semanticTokens.data = [...semanticTokens.data, ...semanticTokenCache.data];
}
return semanticTokens;
});
/* Provide formatting */
connection.onDocumentFormatting((params, tokens) => {
const { textDocument, options } = params;
const sourceDocument = documents.get(textDocument.uri);
if (!sourceDocument) {
return [];
} /* File doesn't exist */
/* Get contents of the document */
const documentContents = sourceDocument.getText();
/* Get the filename */
const filePath = (0, url_1.fileURLToPath)(textDocument.uri);
const filename = path.basename(filePath);
/* Get settings for author name and tabspaces and return formatted text */
return getSettings(connection, sourceDocument.uri).then((settings) => {
const formattedText = (0, formattingProvider_1.formatDocument)(filename, documentContents, settings.tabspaces, settings.fileAuthor, settings.formatPatterns, settings.autoFormatHeader, settings.wrapAt);
return [node_1.TextEdit.replace((0, helpers_2.fullRange)(sourceDocument), formattedText)];
});
});
/* Provide hover features */
connection.onHover(async (params) => {
const document = documents.get(params.textDocument.uri);
if (!document) {
return null;
}
return (0, hoverProvider_1.getHoverFor)([...hoverCacheBuiltin, ...hoverCache], document.getText(), document.offsetAt(params.position));
});
/* Provide go-to definitions functionality */
connection.onDefinition((params) => {
const { textDocument, position } = params;
const doc = documents.get(textDocument.uri);
if (!doc) {
return null;
}
const content = doc.getText();
const offset = doc.offsetAt(position);
// const loc = getGoToDefinition(content, offset, globalAST);
const loc = (0, gotoProvider_1.getGoToDefinitionAdvanced)(content, offset, pssAST);
return loc;
});
/* Provide go-to declarations functionality */
connection.onDeclaration((params) => {
const { textDocument, position } = params;
const doc = documents.get(textDocument.uri);
if (!doc) {
return null;
}
const content = doc.getText();
const offset = doc.offsetAt(position);
const loc = (0, gotoProvider_1.getGoToDeclarationsAdvanced)(content, offset, pssAST);
return loc;
});
connection.onReferences((params) => {
const { textDocument, position } = params;
const doc = documents.get(textDocument.uri);
if (!doc) {
return null;
}
const content = doc.getText();
const offset = doc.offsetAt(position);
const loc = (0, gotoProvider_1.getReferencesAdvanced)(content, offset, pssAST);
return loc;
});
/* Handle custom requests */
/* This request is for server to create doxygen comments when requested by server */
connection.onRequest(dataTypes_1.RequestDoxygenGeneration, async (params) => {
const { line, lineNumber, fileURI } = params;
let response = {
content: '',
keyword: ''
};
const trimmedLine = line.trim();
const words = trimmedLine.split(/\s+/);
for (const word of words) {
const trimmedWord = word.trim();
// Split identifiers with dots and process each part
const identifiers = trimmedWord.split('.').filter(id => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id));
for (const identifier of identifiers) {
if (!(identifier in keywords_1.keywords.list) && !(identifier in builtinFunctions_1.builtInSignatures)) {
const objNode = (0, helpers_2.getNodeFromNameArray)(pssAST, identifier);
if (objNode) {
response.keyword = objNode.name;
response.content = (0, objectCommentsProvider_1.createCommentsFromNode)(objNode);
return response;
}
}
}
}
return response;
});
/* Prepare to listen */
documents.listen(connection);
/* Start listening */
connection.listen();