Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 171 additions & 46 deletions extension.js
Original file line number Diff line number Diff line change
@@ -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 '';
Expand All @@ -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) {
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -146,21 +153,125 @@ 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 a string.
* Each tab is counted as tabSize spaces (default 4).
*/
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);
}

/**
* 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, 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];
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, tabSize);
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 tabSize for clean alignment.
commentCol = Math.ceil(commentCol / tabSize) * tabSize;
}

// 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, tabSize);

let padding;
if (codePart.trim().length === 0) {
// Continuation line: pad from column 0 to commentCol.
padding = makePadding(commentCol, insertSpaces, tabSize);
} else {
const spaces = commentCol - codeLen;
padding = makePadding(spaces > 0 ? spaces : MIN_GAP, insertSpaces, tabSize);
}

result[j] = (codePart + padding + comment).trimEnd();
}
}

return formattedLine.trimEnd();
return result;
}

function deactivate() { }
Expand All @@ -176,28 +287,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, options);

// 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
Expand Down