'use strict'; const assert = require('node:assert/strict'); const { test } = require('node:test'); const { lineDepths, indentationEdits } = require('../src/indentation'); test('nested branches and each implemented block use one Tab per level', () => { const lines = [ 'PROCEDURE Demo()', 'FOR I <- 1 TO 3', 'IF X THEN', 'WHILE Y DO', 'OUTPUT X', 'ENDWHILE', 'ELSE', 'REPEAT', 'OUTPUT Y', 'UNTIL Done', 'ENDIF', 'NEXT I', 'ENDPROCEDURE', 'FUNCTION Value() RETURNS INTEGER', 'RETURN 1', 'ENDFUNCTION', 'TYPE Record', 'DECLARE X : INTEGER', 'ENDTYPE' ]; assert.deepEqual(lineDepths(lines), [0, 1, 2, 3, 4, 3, 2, 3, 4, 3, 2, 1, 0, 0, 1, 0, 0, 1, 0]); }); test('split THEN/DO, single-line TYPE, comments and strings do not add extra levels', () => { const lines = [ 'TYPE Season = (Spring, Summer)', 'TYPE Ptr = ^INTEGER', 'IF Ready', 'THEN // IF DO', 'WHILE Check("DO")', 'DO', 'OUTPUT "ENDIF // IF THEN"', "OUTPUT 'E' // ENDWHILE", 'ENDWHILE', 'ENDIF', 'IFValue <- 1', 'if X then' ]; assert.deepEqual(lineDepths(lines), [0, 0, 0, 0, 1, 1, 2, 2, 1, 0, 0, 0]); }); test('try and catch bodies nest with existing blocks and THROW keeps their depth', () => { const lines = [ 'PROCEDURE Demo()', 'TRY', 'IF Ready THEN', 'THROW', 'ENDIF', 'CATCH', 'TRY', 'THROW', 'CATCH', 'WHILE Ready DO', 'THROW', 'ENDWHILE', 'ENDTRY', 'ENDTRY', 'OUTPUT "Done"', 'ENDPROCEDURE' ]; assert.deepEqual(lineDepths(lines), [0, 1, 2, 3, 2, 1, 2, 3, 2, 3, 4, 3, 2, 1, 1, 0]); assert.deepEqual(lineDepths([ 'CATCH', 'ENDTRY', 'try', 'TRYValue <- 1', 'TRY // CATCH ENDTRY', '// ENDTRY', 'OUTPUT "CATCH ENDTRY"', 'CATCH // TRY', 'THROW', 'ENDTRY', 'OUTPUT "Done"' ]), [0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0]); }); test('catch selection uses earlier TRY context and formatting is idempotent', () => { const lines = ['IF Ready THEN', ' TRY', ' THROW', ' CATCH', ' OUTPUT "Caught"', ' ENDTRY', 'ENDIF']; const edits = indentationEdits(lines, 3, 5); assert.deepEqual(edits, [ { line: 3, length: 4, text: '\t' }, { line: 4, length: 1, text: '\t\t' }, { line: 5, length: 3, text: '\t' } ]); for (const edit of edits) lines[edit.line] = edit.text + lines[edit.line].slice(edit.length); assert.equal(lines[1], ' TRY'); assert.equal(lines[2], ' THROW'); assert.equal(lines[4], '\t\tOUTPUT "Caught"'); assert.deepEqual(indentationEdits(lines, 3, 5), []); }); test('range formatting reads earlier context, preserves text and is idempotent', () => { const lines = ['IF X THEN', ' WHILE Y DO', ' OUTPUT "x" // keep spaces', ' ENDWHILE', 'ENDIF']; const edits = indentationEdits(lines, 2, 3); assert.deepEqual(edits, [ { line: 2, length: 1, text: '\t\t' }, { line: 3, length: 1, text: '\t' } ]); for (const edit of edits) lines[edit.line] = edit.text + lines[edit.line].slice(edit.length); assert.equal(lines[2], '\t\tOUTPUT "x" // keep spaces'); assert.equal(lines[1], ' WHILE Y DO'); assert.deepEqual(indentationEdits(lines, 2, 3), []); assert.deepEqual(indentationEdits(['ENDIF', 'UNTIL X', 'NEXT I'], 0, 2), []); }); test('blank lines are empty when formatted and indented at the caret after Enter', () => { for (const header of [['IF X THEN'], ['TRY'], ['TRY', 'THROW', 'CATCH']]) { const lines = [...header, ' ']; const line = header.length; assert.deepEqual(indentationEdits(lines, line, line), [{ line, length: 3, text: '' }]); assert.deepEqual(indentationEdits(lines, line, line, true), [{ line, length: 3, text: '\t' }]); } }); test('escaped quotes keep block keywords inside literals masked', () => { assert.deepEqual(lineDepths([ String.raw`IF Text = "quoted \" THEN"`, 'THEN', String.raw`OUTPUT "escaped \" ENDIF //"`, 'ENDIF' ]), [0, 0, 1, 0]); }); test('preprocessor conditions preserve ordinary block depth without adding indentation', () => { const lines = [ 'PP_DEFINE Flag', 'PP_IFDEF Flag', 'PROCEDURE Demo()', 'PP_IFNDEF Other', 'IF Ready THEN', 'OUTPUT 1', 'ENDIF', 'PP_ENDIF', 'PP_UNDEF Flag', 'ENDPROCEDURE', 'PP_ENDIF' ]; assert.deepEqual(lineDepths(lines), [0, 0, 0, 1, 1, 2, 1, 1, 1, 0, 0]); assert.deepEqual(indentationEdits(['PP_IFDEF Flag', 'OUTPUT 1', 'PP_ENDIF'], 0, 2), []); });