From 378d105e863583226371ac0f89ce86d37fcf7b0e Mon Sep 17 00:00:00 2001 From: "Stoyan Krastev (skkdevcraft)" <163518570+skkdevcraft@users.noreply.github.com> Date: Mon, 11 May 2026 15:18:06 +0000 Subject: [PATCH 1/2] feat: implement per-block alignment --- extension.js | 200 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 154 insertions(+), 46 deletions(-) diff --git a/extension.js b/extension.js index 212d95e..d547784 100644 --- a/extension.js +++ b/extension.js @@ -1,5 +1,29 @@ const vscode = require('vscode'); +/** + * Finds the index of the first comment character (';' or '#') that is + * outside of a string literal. Returns -1 if none found. + */ +function findCommentStart(text) { + let inString = false; + let stringChar = ''; + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if ((char === "'" || char === '"') && (i === 0 || text[i - 1] !== '\\')) { + if (!inString) { + inString = true; + stringChar = char; + } else if (stringChar === char) { + inString = false; + } + } + if ((char === ';' || char === '#') && !inString) { + return i; + } + } + return -1; +} + function formatAsmLine(line) { let text = line.trim(); if (!text) return ''; @@ -9,31 +33,18 @@ function formatAsmLine(line) { const originalIndent = leadingWhitespaceMatch ? leadingWhitespaceMatch[0] : ''; // Pass through full-line comments while preserving their original indentation - if (text.startsWith(';')) { - return originalIndent + text; + if (text.startsWith(';') || text.startsWith('#')) { + return '\x00' + text; } let comment = ''; let codePart = text; - let inString = false; - let stringChar = ''; - - // 1. Safely extract comment (ignoring ';' inside strings) - for (let i = 0; i < text.length; i++) { - let char = text[i]; - if ((char === "'" || char === '"') && (i === 0 || text[i-1] !== '\\')) { - if (!inString) { - inString = true; - stringChar = char; - } else if (stringChar === char) { - inString = false; - } - } - if (char === ';' && !inString) { - comment = text.slice(i).trim(); - codePart = text.slice(0, i).trim(); - break; - } + + // 1. Safely extract comment (ignoring comment chars inside strings) + const commentIdx = findCommentStart(text); + if (commentIdx !== -1) { + comment = text.slice(commentIdx).trim(); + codePart = text.slice(0, commentIdx).trim(); } if (!codePart) { @@ -56,14 +67,10 @@ function formatAsmLine(line) { if (isBypass) { let formattedLine = originalIndent + codePart; - // Only format the comment alignment + // Comment alignment is handled externally by the group-alignment pass; + // just attach it with a placeholder sentinel so the caller can replace it. if (comment) { - const currentLength = formattedLine.replace(/\t/g, ' ').length; - const commentColumn = 48; - let paddingSpaces = commentColumn - currentLength; - if (paddingSpaces <= 0) paddingSpaces = 4; - const tabsCount = Math.ceil(paddingSpaces / 4); - formattedLine += '\t'.repeat(tabsCount > 0 ? tabsCount : 1) + comment; + formattedLine += '\x00' + comment; } return formattedLine.trimEnd(); } @@ -105,7 +112,7 @@ function formatAsmLine(line) { for (let i = 0; i < rawOperands.length; i++) { let char = rawOperands[i]; - if ((char === "'" || char === '"') && (i === 0 || rawOperands[i-1] !== '\\')) { + if ((char === "'" || char === '"') && (i === 0 || rawOperands[i - 1] !== '\\')) { if (!inQuotes) { inQuotes = true; quoteCh = char; @@ -129,7 +136,7 @@ function formatAsmLine(line) { let formattedLine = ''; if (label) { - // FIX: Guarantee at least one space if the label overflows the 16-character column + // Guarantee at least one space if the label overflows the 16-character column if (label.length >= 16) { formattedLine += label + ' '; } else { @@ -146,21 +153,108 @@ function formatAsmLine(line) { } } - // Standard Comment Alignment + // Attach comment with sentinel — replaced by group-alignment pass if (comment) { - if (formattedLine.length > 0) { - const currentLength = formattedLine.replace(/\t/g, ' ').length; - const commentColumn = 48; - let paddingSpaces = commentColumn - currentLength; - if (paddingSpaces <= 0) paddingSpaces = 4; - const tabsCount = Math.ceil(paddingSpaces / 4); - formattedLine += '\t'.repeat(tabsCount > 0 ? tabsCount : 1) + comment; + formattedLine += '\x00' + comment; + } + + return formattedLine.trimEnd(); +} + +/** + * Measures the visual length of the code portion of a line (before the sentinel). + * Tabs count as 4 spaces. + */ +function visualLength(text) { + return text.replace(/\t/g, ' ').length; +} + +/** + * Takes an array of already-formatted lines (with \x00 sentinel before each + * inline comment) and rewrites them so that comments within each group are + * aligned to the same column. + * + * GROUP RULES: + * - A group is a maximal consecutive run of lines that ALL carry a sentinel. + * - Any line without a sentinel (blank, code-only, full-line comment) breaks + * the group. This means two commented lines separated by even one + * uncommented line are in different groups with independent columns. + * + * COLUMN RULES: + * - commentCol = maxCodeLen + MIN_GAP, rounded up to the next multiple of 4. + * - If no line in the group has code (pure continuation block), commentCol = 0 + * and comments start at the beginning of the line. + * + * CONTINUATION LINES: + * - A line whose code part is empty (only whitespace before the sentinel) is + * a continuation line. It is padded to the same commentCol as the group. + */ +function alignCommentGroups(lines) { + const MIN_GAP = 1; // minimum spaces between end of code and start of comment + + const result = [...lines]; + let i = 0; + + while (i < result.length) { + // Only lines with a sentinel start or continue a group. + if (!result[i].includes('\x00')) { + i++; + continue; + } + + // Collect a contiguous run of sentinel-carrying lines. + // Any line without a sentinel (blank or code-only) ends the group. + const groupStart = i; + while (i < result.length && result[i].includes('\x00')) { + i++; + } + const groupEnd = i; // exclusive + + // Measure the longest code part in this group. + let maxCodeLen = 0; + let hasCodeLines = false; + for (let j = groupStart; j < groupEnd; j++) { + const sentinelIdx = result[j].indexOf('\x00'); + const codePart = result[j].slice(0, sentinelIdx); + if (codePart.trim().length > 0) { + hasCodeLines = true; + const len = visualLength(codePart); + if (len > maxCodeLen) maxCodeLen = len; + } + } + + // Determine the comment column for this group. + let commentCol; + if (!hasCodeLines) { + // Pure comment-continuation block — flush left. + commentCol = 0; } else { - formattedLine = comment; + commentCol = maxCodeLen + MIN_GAP; + // Round up to the next multiple of 4 for clean alignment. + commentCol = Math.ceil(commentCol / 4) * 4; + } + + // Rewrite every line in the group. + for (let j = groupStart; j < groupEnd; j++) { + const sentinelIdx = result[j].indexOf('\x00'); + const codePart = result[j].slice(0, sentinelIdx); + const comment = result[j].slice(sentinelIdx + 1); + const codeLen = visualLength(codePart); + + let padding; + if (codePart.trim().length === 0) { + // Continuation line: pad from column 0 to commentCol. + padding = ' '.repeat(commentCol); + } else { + const spaces = commentCol - codeLen; + padding = ' '.repeat(spaces > 0 ? spaces : MIN_GAP); + } + + result[j] = (codePart + padding + comment).trimEnd(); } } - return formattedLine.trimEnd(); + return result; } function deactivate() { } @@ -176,28 +270,42 @@ function activate(context) { { language: 'masm' }, { language: 'fasm' } ]; + const provider = vscode.languages.registerDocumentFormattingEditProvider(supportedLanguages, { provideDocumentFormattingEdits(document, options, token) { - const edits = []; + + // Pass 1: format each line individually (comments marked with \x00 sentinel) + const formattedLines = []; for (let i = 0; i < document.lineCount; i++) { if (token.isCancellationRequested) return []; const line = document.lineAt(i); try { - const formattedText = formatAsmLine(line.text); - if (line.text !== formattedText) { - edits.push(vscode.TextEdit.replace(line.range, formattedText)); - } + formattedLines.push(formatAsmLine(line.text)); } catch (e) { console.error(`asm-formatter: line ${i + 1}:`, e); + formattedLines.push(line.text); + } + } + + // Pass 2: align comment groups across consecutive commented lines + const alignedLines = alignCommentGroups(formattedLines); + + // Build edits + const edits = []; + for (let i = 0; i < document.lineCount; i++) { + const original = document.lineAt(i).text; + const formatted = alignedLines[i]; + if (original !== formatted) { + edits.push(vscode.TextEdit.replace(document.lineAt(i).range, formatted)); } } return edits; - } }); context.subscriptions.push(provider); } + module.exports = { activate, deactivate From 4cbfc0639f598ba2de8063e8e0c6c3af602cc72c Mon Sep 17 00:00:00 2001 From: "Stoyan Krastev (skkdevcraft)" <163518570+skkdevcraft@users.noreply.github.com> Date: Mon, 11 May 2026 19:12:24 +0300 Subject: [PATCH 2/2] fix: no hard-coded tabs. use the editor configured method - space or tabs --- extension.js | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/extension.js b/extension.js index d547784..3847dc3 100644 --- a/extension.js +++ b/extension.js @@ -162,11 +162,26 @@ function formatAsmLine(line) { } /** - * Measures the visual length of the code portion of a line (before the sentinel). - * Tabs count as 4 spaces. + * Measures the visual length of a string. + * Each tab is counted as tabSize spaces (default 4). */ -function visualLength(text) { - return text.replace(/\t/g, ' ').length; +function visualLength(text, tabSize = 4) { + return text.replace(/\t/g, ' '.repeat(tabSize)).length; +} + +/** + * Produces a padding string of exactly `columns` visual columns using either + * spaces only (insertSpaces=true) or a mix of tabs and spaces (insertSpaces=false). + */ +function makePadding(columns, insertSpaces, tabSize) { + if (columns <= 0) return ''; + if (insertSpaces) { + return ' '.repeat(columns); + } + // Use as many tabs as possible, then fill the remainder with spaces. + const tabCount = Math.floor(columns / tabSize); + const spaceCount = columns % tabSize; + return '\t'.repeat(tabCount) + ' '.repeat(spaceCount); } /** @@ -189,7 +204,9 @@ function visualLength(text) { * - A line whose code part is empty (only whitespace before the sentinel) is * a continuation line. It is padded to the same commentCol as the group. */ -function alignCommentGroups(lines) { +function alignCommentGroups(lines, tabOptions) { + const insertSpaces = tabOptions?.insertSpaces ?? true; + const tabSize = tabOptions?.tabSize ?? 4; const MIN_GAP = 1; // minimum spaces between end of code and start of comment const result = [...lines]; @@ -218,7 +235,7 @@ function alignCommentGroups(lines) { const codePart = result[j].slice(0, sentinelIdx); if (codePart.trim().length > 0) { hasCodeLines = true; - const len = visualLength(codePart); + const len = visualLength(codePart, tabSize); if (len > maxCodeLen) maxCodeLen = len; } } @@ -230,8 +247,8 @@ function alignCommentGroups(lines) { commentCol = 0; } else { commentCol = maxCodeLen + MIN_GAP; - // Round up to the next multiple of 4 for clean alignment. - commentCol = Math.ceil(commentCol / 4) * 4; + // Round up to the next multiple of tabSize for clean alignment. + commentCol = Math.ceil(commentCol / tabSize) * tabSize; } // Rewrite every line in the group. @@ -239,15 +256,15 @@ function alignCommentGroups(lines) { const sentinelIdx = result[j].indexOf('\x00'); const codePart = result[j].slice(0, sentinelIdx); const comment = result[j].slice(sentinelIdx + 1); - const codeLen = visualLength(codePart); + const codeLen = visualLength(codePart, tabSize); let padding; if (codePart.trim().length === 0) { // Continuation line: pad from column 0 to commentCol. - padding = ' '.repeat(commentCol); + padding = makePadding(commentCol, insertSpaces, tabSize); } else { const spaces = commentCol - codeLen; - padding = ' '.repeat(spaces > 0 ? spaces : MIN_GAP); + padding = makePadding(spaces > 0 ? spaces : MIN_GAP, insertSpaces, tabSize); } result[j] = (codePart + padding + comment).trimEnd(); @@ -288,7 +305,7 @@ function activate(context) { } // Pass 2: align comment groups across consecutive commented lines - const alignedLines = alignCommentGroups(formattedLines); + const alignedLines = alignCommentGroups(formattedLines, options); // Build edits const edits = [];