UNPKG

pss-langserver

Version:

A Language server for the Portable Stimulus Standard

467 lines (466 loc) 19.8 kB
"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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.formatDocument = formatDocument; exports.formatFileHeader = formatFileHeader; const formattingHelper_1 = __importStar(require("./formattingHelper")); const fs_extra_1 = require("fs-extra"); const path = __importStar(require("path")); function formatDocument(filePath, text, tabspace, author, patterns, formatHeader, maxColumns) { const fileName = path.basename(filePath); let doc = text; if (formatHeader) { const birthTime = (0, formattingHelper_1.formatDate)((0, fs_extra_1.statSync)(filePath).birthtime); const currentTime = (0, formattingHelper_1.formatDate)(new Date()); doc = formatFileHeader(text, fileName, birthTime, currentTime, author); } doc = formatCurlyBraces(doc); doc = formatCommas(doc); doc = formatMultilineComments(doc); doc = addNewlinesAfterSemicolons(doc); let lines = doc.split('\n'); const formattedLines = []; let indentLevel = 0; let braceDepth = 0; let isInBlockComment = false; const indentLevels = []; const processedLines = []; // First pass: Calculate indentation and process lines for (let line of lines) { if (line.trim() === '') { processedLines.push(''); indentLevels.push(0); continue; } const trimmedLine = line.trim(); let formattedLine = formatOperators(trimmedLine); formattedLine = formatSingleLineComments(formattedLine); if (formattedLine.startsWith("*")) { formattedLine = ` ${formattedLine}`; } processedLines.push(formattedLine); if (trimmedLine[0] === '}' && !isInBlockComment) { indentLevel = Math.max(indentLevel - tabspace, 0); } if ((formattedLine.startsWith(')') || formattedLine.startsWith(']')) && !isInBlockComment) { braceDepth = Math.max(braceDepth - 1, 0); } const totalIndent = indentLevel + braceDepth * tabspace; indentLevels.push(totalIndent); const { depthChange, endsInBlockComment } = (0, formattingHelper_1.getBraceDepthChange)(line, isInBlockComment); isInBlockComment = endsInBlockComment; braceDepth += depthChange; braceDepth = Math.max(braceDepth, 0); if (formattedLine.endsWith('{') && !isInBlockComment && !(/\/\/|\/\*/.test(formattedLine))) { indentLevel += tabspace; } } // Apply indentation const indentedLines = processedLines.map((line, idx) => ' '.repeat(indentLevels[idx]) + line); // Align patterns (0, formattingHelper_1.default)(indentedLines, patterns); // Wrap lines for (let idx = 0; idx < indentedLines.length; idx++) { const line = indentedLines[idx]; const currentIndent = ' '.repeat(indentLevels[idx]); const wrappedLines = wrapLine(line, currentIndent, tabspace, maxColumns); formattedLines.push(...wrappedLines); } return formattedLines.join('\n'); } function wrapLine(line, currentIndent, tabspace, maxColumns) { if (maxColumns === 0 || line.length <= maxColumns) { return [line]; } const tokens = (0, formattingHelper_1.tokenizeLine)(line); const wrappedLines = []; let currentLine = ''; let isFirstLine = true; let inMultiLineComment = false; let hasWrappedSingleLineComment = false; for (let j = 0; j < tokens.length; j++) { const token = tokens[j]; const separator = currentLine && !['(', ',', ')'].includes(token) && !['(', ','].includes(currentLine.slice(-1)) ? ' ' : ''; const potentialLine = currentLine + separator + token; const indentLength = isFirstLine ? currentIndent.length : currentIndent.length + tabspace; const potentialLength = indentLength + potentialLine.length; // Handle long single-line comment tokens if (token.startsWith('//') && potentialLength > maxColumns) { const commentText = token.substring(2).trimStart(); const words = commentText.split(' '); let commentLines = []; let currentCommentLine = ''; for (const word of words) { const testLine = currentCommentLine ? currentCommentLine + ' ' + word : word; if (indentLength + testLine.length + 3 > maxColumns) { // 3 for "// " commentLines.push('// ' + currentCommentLine.trim()); currentCommentLine = word; } else { currentCommentLine = testLine; } } if (currentCommentLine) { commentLines.push('// ' + currentCommentLine.trim()); } if (currentLine) { wrappedLines.push((isFirstLine ? currentIndent : currentIndent + ' '.repeat(tabspace)) + currentLine.trimStart()); } if (isFirstLine) { wrappedLines.push(currentIndent + commentLines[0]); for (let k = 1; k < commentLines.length; k++) { wrappedLines.push(currentIndent + ' '.repeat(tabspace) + commentLines[k]); } } else { commentLines.forEach(cl => wrappedLines.push(currentIndent + ' '.repeat(tabspace) + cl)); } isFirstLine = false; hasWrappedSingleLineComment = true; currentLine = ''; } // Handle long multi-line comment tokens else if (token.startsWith('/*') && token.endsWith('*/') && potentialLength > maxColumns) { const commentText = token.substring(2, token.length - 2).trim(); const words = commentText.split(' '); let commentLines = []; let currentCommentLine = ''; for (const word of words) { const testLine = currentCommentLine ? currentCommentLine + ' ' + word : word; if (indentLength + testLine.length + 4 > maxColumns) { // 4 for "* " commentLines.push('* ' + currentCommentLine.trim()); currentCommentLine = word; } else { currentCommentLine = testLine; } } if (currentCommentLine) { commentLines.push('* ' + currentCommentLine.trim()); } if (currentLine) { wrappedLines.push((isFirstLine ? currentIndent : currentIndent + ' '.repeat(tabspace)) + currentLine.trimStart()); } wrappedLines.push((isFirstLine ? currentIndent : currentIndent + ' '.repeat(tabspace)) + '/*'); commentLines.forEach(cl => wrappedLines.push(currentIndent + ' '.repeat(tabspace) + cl)); wrappedLines.push(currentIndent + ' '.repeat(tabspace) + '*/'); isFirstLine = false; currentLine = ''; } else if (potentialLength > maxColumns && currentLine) { const lineIndent = isFirstLine ? currentIndent : currentIndent + ' '.repeat(tabspace); wrappedLines.push(lineIndent + currentLine.trimStart()); isFirstLine = false; if (currentLine.includes('//')) { hasWrappedSingleLineComment = true; } currentLine = (0, formattingHelper_1.handleCommentWrapping)(token, inMultiLineComment, hasWrappedSingleLineComment); } else { currentLine = potentialLine; } if (token === '/*') { inMultiLineComment = true; } else if (token === '*/') { inMultiLineComment = false; } } if (currentLine) { const lineIndent = isFirstLine ? currentIndent : currentIndent + ' '.repeat(tabspace); wrappedLines.push(lineIndent + currentLine.trimStart()); } return wrappedLines; } function formatCurlyBraces(input) { /* * Check for comments on the same line before opening brace * If a comment exists, place the brace before the comment */ input = input.replace(/(\s*)([^\/]*?)(\s*)(\/\/.*|\/\*.*?\*\/)(\s*\n\s*{)/g, '$1$2 {$3$4$5'); /* Remove newlines before opening braces to bring them up */ input = input.replace(/\n\s*{/g, ''); /* Ensure space before opening brace */ input = input.replace(/\s*{/g, ' {'); /* Ensure newline after opening brace */ input = input.replace(/{(?!\s*\n)/g, '{\n'); /* Process closing braces, preserving those in comments, and add newline only if no newlines at all precede */ input = input.replace(/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|([^\s\n])(\s*})(?!\n)/g, (match, comment, p1, p2) => { if (comment) { return comment; } // Preserve comment unchanged // Check if there's already a newline in the whitespace before the closing brace if (p2.includes('\n')) { return p1 + p2; } return p1 + '\n' + p2; // Add newline before closing brace only if directly after code }); /* Add newline after } unless followed by newline, semicolon, or comment */ input = input.replace(/}(?!\s*(?:\n|;|\/\/))/g, '}\n'); /* Add newline after } followed by a multiline comment if newline is missing */ input = input.replace(/}(\s*\/\*[\s\S]*?\*\/)(?!\n)/g, '}$1\n'); return input; } function addNewlinesAfterSemicolons(input) { return input.replace(/(\/\/.*|\/\*[\s\S]*?\*\/|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|;/g, (match, commentOrString, offset, string) => { if (commentOrString) { // If it's a comment or string literal, return it unchanged return commentOrString; } else { // It's a semicolon; determine if a newline should be added let next = offset + 1; // Skip horizontal whitespace (tabs and spaces) after the semicolon while (next < string.length && /[\t ]/.test(string[next])) { next++; } // Check what follows the semicolon if (next >= string.length || string[next] === '\n') { // Semicolon is at end of string or followed by a newline; no extra newline needed return ';'; } else if (string.startsWith('//', next) || string.startsWith('/*', next)) { // Semicolon is followed by a comment; do not add newline return ';'; } else { // Semicolon is followed by code; add a newline return ';\n'; } } }); } function formatCommas(input) { return input.replace(/,(?=\S)/g, ', '); } function formatMultilineComments(documentText) { return documentText.replace(/(\/\*\*?)([\s\S]*?)\*\//g, (match, openingBlock, commentBody) => { const opening = openingBlock; const closing = '*/'; if (!commentBody.includes('\n')) { return `${opening} ${commentBody.trim()} ${closing}`.replace(/\s+/g, ' '); } const newlineIndex = commentBody.indexOf('\n'); let firstPart = ''; let rest = commentBody; if (newlineIndex !== -1) { firstPart = commentBody.substring(0, newlineIndex).trim(); rest = commentBody.substring(newlineIndex + 1); } else { firstPart = commentBody.trim(); rest = ''; } if (/^\*+$/.test(firstPart)) { let formattedComment = `${opening} ${firstPart}`; if (rest) { const lines = rest.split('\n'); const formattedLines = lines.map((line) => { const trimmedLine = line.trim(); if (trimmedLine === '' || trimmedLine === '*') { return null; } else if (trimmedLine.startsWith('*')) { return ` * ${trimmedLine.slice(1).trim()}`; } else { return ` * ${trimmedLine}`; } }).filter((line) => line !== null); formattedComment += '\n' + formattedLines.join('\n'); } formattedComment += '\n' + closing; return formattedComment; } else { const lines = commentBody.split('\n'); const formattedLines = lines.map((line) => { const trimmedLine = line.trim(); if (trimmedLine === '' || trimmedLine === '*') { return null; } else if (trimmedLine.startsWith('*')) { return `* ${trimmedLine.slice(1).trim()}`; } else { return `* ${trimmedLine}`; } }).filter((line) => line !== null); return `${opening}\n${formattedLines.join('\n')}\n${closing}`; } }); } function formatOperators(input) { const multiCharOperators = [ "===", "!==", "==", "!=", "<=", ">=", "=>", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "&&", "||", "++", "--", "<<", ">>", ">>>", "**" ]; multiCharOperators.sort((a, b) => b.length - a.length); const operatorRegex = new RegExp(`(${multiCharOperators.map(op => op.replace(/[-+*/%^=<>!&|]/g, '\\$&')).join('|')})|([+\\-*/%^=<>!&|])`, 'g'); function formatExpression(expression) { return expression.replace(operatorRegex, (match, multiOp, singleOp) => { if (multiOp) { return ` ${multiOp} `; } else if (singleOp) { return ` ${singleOp} `; } return match; }).replace(/\s+/g, ' ').trim(); } let result = ""; let i = 0; let bracketDepth = 0; let currentSegment = ""; while (i < input.length) { if (input[i] === '/' && (input[i + 1] === '/' || input[i + 1] === '*')) { if (currentSegment) { result += bracketDepth > 0 ? formatExpression(currentSegment) : currentSegment; currentSegment = ""; } if (input[i + 1] === '/') { const commentEnd = input.indexOf('\n', i) === -1 ? input.length : input.indexOf('\n', i); result += input.substring(i, commentEnd); i = commentEnd; } else { const commentEnd = input.indexOf('*/', i) + 2; result += input.substring(i, commentEnd); i = commentEnd; } } else if (input[i] === '(') { if (currentSegment) { result += bracketDepth > 0 ? formatExpression(currentSegment) : currentSegment; currentSegment = ""; } bracketDepth++; result += '('; i++; } else if (input[i] === ')') { if (currentSegment) { result += bracketDepth > 0 ? formatExpression(currentSegment) : currentSegment; currentSegment = ""; } bracketDepth--; result += ')'; i++; } else { currentSegment += input[i]; i++; } } if (currentSegment) { result += bracketDepth > 0 ? formatExpression(currentSegment) : currentSegment; } return result; } function formatSingleLineComments(line) { line = line.replace(/([^:])\/\/(?! )/g, '$1 // '); line = line.replace(/([^:])\/\/(?! )/g, '$1 // '); return line; } function formatFileHeader(content, fileName, creationDate, lastModifiedDate, author) { const headerRegex = /^\/\*\*[\s\S]*?\*\/\n?/; if (headerRegex.test(content)) { const header = content.match(headerRegex)[0]; const headerLines = header .replace(/^\/\*\*\s*\n/, '') .replace(/\s*\*\/\s*$/, '') .split('\n') .map(line => line.replace(/^\s*\*\s?/, '')) .filter(line => line !== ''); const hasFileTag = headerLines.some(line => line.trim().startsWith('@file')); const hasAuthorTag = headerLines.some(line => line.trim().startsWith('@author')); const hasDateTag = headerLines.some(line => line.trim().startsWith('@date')); const hasLastModified = headerLines.some(line => line.trim().startsWith('Last Modified on:')); let newHeaderLines = []; if (!hasFileTag) { newHeaderLines.push(`@file ${fileName}`); } for (const line of headerLines) { if (!line.trim().startsWith('@author') && !line.trim().startsWith('Last Modified on:') && (hasDateTag || !line.trim().startsWith('@date'))) { newHeaderLines.push(line); } } if (!hasDateTag) { newHeaderLines.push(`@date ${creationDate}`); } if (!hasAuthorTag) { newHeaderLines.push(`@author ${author}`); } else { const authorLine = headerLines.find(line => line.trim().startsWith('@author')); newHeaderLines.push(authorLine || `@author ${author}`); } newHeaderLines.push(`Last Modified on: ${lastModifiedDate}`); const newHeader = [ '/**', ...newHeaderLines.map(line => ` * ${line}`), ' */' ].join('\n') + '\n'; return content.replace(headerRegex, newHeader); } const newHeader = `/** * @file ${fileName} * @brief * @date ${creationDate} * @author ${author} * Last Modified on: ${lastModifiedDate} */ `; return newHeader + content; }