import { test } from 'brittle'
import { buildCells, CELL } from '../lib/floor/cells.js'
import { deriveFile } from '../lib/derive.js'
// The renderer draws a file as a grid of character cells, and each cell carries
// ONE byte of derived meaning (spec V5). This is that byte, and it is the only
// part of the renderer that can be tested without a GPU — so it is where the
// correctness lives.
function grid (source, path = '/x/t.js') {
return buildCells(source, deriveFile(source, path))
}
function cellAt (g, line, col) {
return g.cells[(line - 1) * g.cols + col]
}
function charAt (g, line, col) {
return String.fromCharCode(g.text[(line - 1) * g.cols + col])
}
test('floor cells: the grid is the text, verbatim', (t) => {
const g = grid('const a = 1\nconst bb = 22\n')
t.is(g.rows, 3, '3 rows — the trailing newline makes an empty last line')
t.is(g.cols, 13, 'cols is the longest line')
t.is(charAt(g, 1, 0), 'c', 'first char')
t.is(charAt(g, 2, 6), 'b', 'second line, seventh column')
t.is(g.text[(1 - 1) * g.cols + 12], 0, 'short lines are padded with NUL, not spaces')
})
test('floor cells: a tab is expanded, and ranges still land correctly', (t) => {
// codopticon draws a tab as ONE cell, so tab-indented code looks unindented.
// GDScript is tab-indented, so this is not optional for us.
const g = buildCells('if (x) {\n\treturn 1\n}\n', deriveFile('if (x) {\n\treturn 1\n}\n', '/x/t.js'), { tabWidth: 4 })
// The tab leaves EMPTY cells (NUL) rather than spaces — the renderer discards
// empty cells, so emitting spaces would be pure work for no pixels.
t.is(g.text[(2 - 1) * g.cols + 0], 0, 'the tab occupies empty cells')
t.is(g.text[(2 - 1) * g.cols + 3], 0, 'all four of them')
t.is(charAt(g, 2, 4), 'r', 'and `return` starts at column 4')
t.is(g.tabWidth, 4, 'reports the width it used')
})
test('floor cells: non-ASCII is replaced and counted, never mangled', (t) => {
// codetree's own source is full of em-dashes. codopticon indexes a 128-entry
// glyph table by raw byte, so this is exactly where it would break.
const g = grid('// a — b\nconst x = 1\n')
t.is(g.nonAscii, 1, 'one non-ASCII character counted')
t.is(g.text[(1 - 1) * g.cols + 5], CELL.NON_ASCII_GLYPH, 'replaced by the placeholder slot')
t.is(charAt(g, 1, 7), 'b', 'and the rest of the line keeps its columns')
})
test('floor cells: a pathological line is truncated, not allowed to size the grid', (t) => {
const long = 'const x = "' + 'y'.repeat(900) + '"\n'
const g = buildCells(long + 'const a = 1\n', deriveFile(long + 'const a = 1\n', '/x/t.js'), { colLimit: 120 })
t.is(g.cols, 120, 'the grid is capped')
t.ok(g.truncatedLines > 0, 'and says how many lines it clipped')
})
test('floor cells: zone depth is in the low bits', (t) => {
const src = [
'function f (rows) {', // 1
' for (const r of rows) {', // 2 zone depth 1
' if (r.skip) {', // 3 zone depth 2
' continue', // 4 depth 2
' }', // 5
' }', // 6
'}', // 7
''
].join('\n')
const g = grid(src)
const depth = (line, col) => cellAt(g, line, col) & CELL.DEPTH_MASK
t.is(depth(1, 0), 0, 'the function signature is outside every zone')
t.is(depth(2, 2), 1, 'the for line is depth 1')
t.is(depth(3, 4), 2, 'the if inside it is depth 2')
t.is(depth(4, 6), 2, 'its body is depth 2')
t.is(depth(7, 0), 0, 'and we are back out at the end')
})
test('floor cells: depth saturates instead of overflowing its bits', (t) => {
let src = 'function f (a) {\n'
for (let i = 0; i < 10; i++) src += ' '.repeat(i + 1) + 'if (a) {\n'
src += ' '.repeat(12) + 'return 1\n'
for (let i = 10; i > 0; i--) src += ' '.repeat(i) + '}\n'
src += '}\n'
const g = grid(src)
let max = 0
for (const c of g.cells) max = Math.max(max, c & CELL.DEPTH_MASK)
t.is(max, CELL.DEPTH_MASK, 'clamped to the mask, never wrapping to 0')
t.ok(g.maxDepth > CELL.DEPTH_MASK, 'while still reporting the real depth: ' + g.maxDepth)
})
test('floor cells: operation, exit and name bits mark their own spans', (t) => {
const src = 'function f (a) {\n const b = a + 1\n return b\n}\n'
const g = grid(src)
// `a + 1` is an operation; the `+` sits at column 14 of line 2.
t.ok(cellAt(g, 2, 14) & CELL.OP, 'the + is inside an operation span')
t.absent(cellAt(g, 1, 0) & CELL.OP, 'the function keyword is not')
// `const b = a + 1` names a wire — and the span starts at the IDENTIFIER, not
// at `const`: a declarator's range excludes the keyword, which belongs to the
// enclosing declaration.
t.absent(cellAt(g, 2, 2) & CELL.NAME, 'the `const` keyword is outside the span')
t.ok(cellAt(g, 2, 8) & CELL.NAME, 'the name `b` starts it')
t.ok(cellAt(g, 2, 14) & CELL.NAME, 'and it runs to the end of the initialiser')
// `return b` is an exit.
t.ok(cellAt(g, 3, 2) & CELL.EXIT, 'the return is marked as an exit')
t.absent(cellAt(g, 2, 2) & CELL.EXIT, 'the declaration is not an exit')
})
test('floor cells: a partial model still renders, and says so', (t) => {
const src = 'static func f():\n\tfor r in [1]:\n\t\tcontinue\n'
const g = buildCells(src, deriveFile(src, '/x/t.gd'))
t.ok(g.rows > 0, 'it still produced a grid')
t.ok(g.partial, 'and carries the model\'s partial flag forward to the view')
})
test('floor cells: purity — same input, identical output', (t) => {
const src = 'function f (a) {\n if (a) return 1\n return 0\n}\n'
const a = grid(src)
const b = grid(src)
t.alike(Array.from(a.cells), Array.from(b.cells), 'cells identical')
t.alike(Array.from(a.text), Array.from(b.text), 'text identical')
})