pss-langserver
Version:
A Language server for the Portable Stimulus Standard
237 lines (196 loc) • 8.56 kB
text/typescript
/*
* 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/>.
*/
function alignTextElements(input: string, patterns: string[]): string {
// Split the input into lines
const lines = input.split('\n');
// Process each formatting pattern
let contents = lines;
let isEndComment = false;
if (patterns.length == 0) {
return input; /* Assume user doesn't want any formatting */
}
patterns.forEach(pattern => {
if (pattern === "//") {
isEndComment = true;
}
contents = findAndAlignConsecutivePatterns(contents, pattern, isEndComment);
});
// const colonFormatted = findAndAlignConsecutivePatterns(lines, ':');
// const formattedEquals = findAndAlignConsecutivePatterns(colonFormatted, '=');
// const formatted = findAndAlignConsecutivePatterns(formattedEquals, '//', true);
// Join the formatted lines and return
return contents.join('\n');
}
function findAndAlignConsecutivePatterns(lines: string[], pattern: string, isEndComment: boolean = false): string[] {
const result = [...lines];
let i = 0;
let blockStart = 0;
let inBlock = false;
// Group lines by code blocks (determined by indentation)
while (i < result.length) {
// Detect blocks based on indentation or braces
if (!inBlock && result[i].includes('{')) {
blockStart = i;
inBlock = true;
}
if (inBlock && result[i].includes('}')) {
// Process this block separately
processBlockForAlignment(result, blockStart, i + 1, pattern, isEndComment);
inBlock = false;
}
i++;
}
// Process the top level (non-block) lines
processBlockForAlignment(result, 0, result.length, pattern, isEndComment, true);
return result;
}
function processBlockForAlignment(lines: string[], start: number, end: number, pattern: string, isEndComment: boolean, isTopLevel: boolean = false): void {
// Find consecutive lines with the pattern at the same indent level
let currentIndent = -1;
let blockStartLine = start;
let consecutiveCount = 0;
for (let i = start; i < end; i++) {
const line = lines[i].trim();
// Skip empty lines or block delimiters
if (line === '' || line === '{' || line === '}') {
continue;
}
// Calculate indent level
const indent = lines[i].length - lines[i].trimStart().length;
// If moving to a different indent level, process the previous block
if (currentIndent !== -1 && indent !== currentIndent) {
if (consecutiveCount >= 2) {
alignPatternInBlock(lines, blockStartLine, i, pattern, isEndComment);
}
blockStartLine = i;
consecutiveCount = 0;
}
currentIndent = indent;
// Check if line contains the pattern outside of brackets
if (hasPatternOutsideBrackets(lines[i], pattern)) {
consecutiveCount++;
} else {
// If we had consecutive lines and now found a non-matching one, process the block
if (consecutiveCount >= 2) {
alignPatternInBlock(lines, blockStartLine, i, pattern, isEndComment);
}
blockStartLine = i + 1;
consecutiveCount = 0;
}
}
// Process the last block if needed
if (consecutiveCount >= 2) {
alignPatternInBlock(lines, blockStartLine, end, pattern, isEndComment);
}
}
function hasPatternOutsideBrackets(line: string, pattern: string): boolean {
// Skip if pattern isn't in the line
if (!line.includes(pattern)) return false;
// Track bracket nesting and quotes
let inSingleQuote = false;
let inDoubleQuote = false;
let bracketDepth = 0; // Tracks all bracket types
for (let i = 0; i < line.length; i++) {
const char = line[i];
// Handle quotes (toggle state if not escaped)
if (char === "'" && (i === 0 || line[i - 1] !== '\\')) {
inSingleQuote = !inSingleQuote;
} else if (char === '"' && (i === 0 || line[i - 1] !== '\\')) {
inDoubleQuote = !inDoubleQuote;
}
// Skip processing if in quotes
if (inSingleQuote || inDoubleQuote) continue;
// Track bracket depth
if (char === '(' || char === '[' || char === '{') {
bracketDepth++;
} else if (char === ')' || char === ']' || char === '}') {
bracketDepth--;
}
// Check for pattern at current position, but only if not inside brackets
if (bracketDepth === 0 && line.substring(i, i + pattern.length) === pattern) {
// Verify it's a standalone pattern (not part of another token)
const beforeChar = i > 0 ? line[i - 1] : ' ';
const afterChar = i + pattern.length < line.length ? line[i + pattern.length] : ' ';
if (/[\s\w]/.test(beforeChar) && /[\s\w=;,)]/.test(afterChar)) {
return true;
}
}
}
return false;
}
function alignPatternInBlock(lines: string[], start: number, end: number, pattern: string, isEndComment: boolean): void {
// Find the longest prefix before the pattern (ignoring patterns inside brackets)
let maxPrefixLength = 0;
let linesToAlign = [];
// First, collect all valid lines and determine max prefix length
for (let i = start; i < end; i++) {
const line = lines[i];
if (!hasPatternOutsideBrackets(line, pattern)) continue;
const { prefix } = getPatternPosition(line, pattern);
maxPrefixLength = Math.max(maxPrefixLength, prefix.length);
linesToAlign.push(i);
}
// Now align each line
for (const i of linesToAlign) {
const line = lines[i];
const { prefix, suffix } = getPatternPosition(line, pattern);
const padding = ' '.repeat(maxPrefixLength - prefix.length);
if (isEndComment) {
// For comments, add more padding
lines[i] = prefix + padding + ' '.repeat(4) + suffix;
} else {
// For other patterns, maintain a single space before and after
lines[i] = prefix + padding + ' ' + pattern + ' ' + suffix.trimStart().substring(pattern.length).trimStart();
}
}
}
function getPatternPosition(line: string, pattern: string): { prefix: string, suffix: string } {
let inSingleQuote = false;
let inDoubleQuote = false;
let bracketDepth = 0;
for (let i = 0; i < line.length; i++) {
const char = line[i];
// Handle quotes
if (char === "'" && (i === 0 || line[i - 1] !== '\\')) {
inSingleQuote = !inSingleQuote;
} else if (char === '"' && (i === 0 || line[i - 1] !== '\\')) {
inDoubleQuote = !inDoubleQuote;
}
// Skip if in quotes
if (inSingleQuote || inDoubleQuote) continue;
// Track bracket depth
if (char === '(' || char === '[' || char === '{') {
bracketDepth++;
} else if (char === ')' || char === ']' || char === '}') {
bracketDepth--;
}
// Found pattern outside brackets
if (bracketDepth === 0 && line.substring(i, i + pattern.length) === pattern) {
// Verify it's a standalone pattern
const beforeChar = i > 0 ? line[i - 1] : ' ';
const afterChar = i + pattern.length < line.length ? line[i + pattern.length] : ' ';
if (/[\s\w]/.test(beforeChar) && /[\s\w=;,)]/.test(afterChar)) {
const prefix = line.substring(0, i).trimEnd();
const suffix = line.substring(i);
return { prefix, suffix };
}
}
}
// Fallback (should not happen if hasPatternOutsideBrackets returned true)
return { prefix: line, suffix: '' };
}
export default alignTextElements;