pss-langserver
Version:
A Language server for the Portable Stimulus Standard
467 lines (466 loc) • 19.8 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;
};
})();
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;
}