UNPKG

pss-langserver

Version:

A Language server for the Portable Stimulus Standard

458 lines (418 loc) 16.9 kB
/* * 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/>. */ import { integer } from "vscode-languageserver"; import alignTextElements, { formatDate, formatExpression, getBraceDepthChange, handleCommentWrapping, tokenizeLine, wrapCommentText } from "./formattingHelper"; import { statSync } from "fs-extra"; import * as path from 'path'; export function formatDocument(filePath: string, text: string, tabspace: number, author: string, patterns: string[], formatHeader: boolean, maxColumns: number): string { const fileName = path.basename(filePath); let doc: string = text; if (formatHeader) { const birthTime = formatDate(statSync(filePath).birthtime); const currentTime = 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: string[] = []; let indentLevel = 0; let braceDepth = 0; let isInBlockComment = false; const indentLevels: number[] = []; const processedLines: string[] = []; // 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 } = 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 alignTextElements(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: string, currentIndent: string, tabspace: number, maxColumns: number): string[] { if (maxColumns === 0 || line.length <= maxColumns) { return [line]; } const tokens: string[] = tokenizeLine(line); const wrappedLines: string[] = []; 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: string[] = []; 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: string[] = []; 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 = 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: string): string { /* * 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: string): string { 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: string): string { return input.replace(/,(?=\S)/g, ', '); } function formatMultilineComments(documentText: string): string { return documentText.replace( /(\/\*\*?)([\s\S]*?)\*\//g, (match: string, openingBlock: string, commentBody: string) => { 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: string) => { const trimmedLine = line.trim(); if (trimmedLine === '' || trimmedLine === '*') { return null; } else if (trimmedLine.startsWith('*')) { return ` * ${trimmedLine.slice(1).trim()}`; } else { return ` * ${trimmedLine}`; } }).filter((line: string | null) => line !== null); formattedComment += '\n' + formattedLines.join('\n'); } formattedComment += '\n' + closing; return formattedComment; } else { const lines = commentBody.split('\n'); const formattedLines = lines.map((line: string) => { const trimmedLine = line.trim(); if (trimmedLine === '' || trimmedLine === '*') { return null; } else if (trimmedLine.startsWith('*')) { return `* ${trimmedLine.slice(1).trim()}`; } else { return `* ${trimmedLine}`; } }).filter((line: string | null) => line !== null); return `${opening}\n${formattedLines.join('\n')}\n${closing}`; } } ); } function formatOperators(input: string): string { 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: string): string { 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: string): string { line = line.replace(/([^:])\/\/(?! )/g, '$1 // '); line = line.replace(/([^:])\/\/(?! )/g, '$1 // '); return line; } export function formatFileHeader(content: string, fileName: string, creationDate: string, lastModifiedDate: string, author: string): string { 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; }