UNPKG

pss-langserver

Version:

A Language server for the Portable Stimulus Standard

465 lines (464 loc) 20.5 kB
#!/usr/bin/env node "use strict"; /* * 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();