53b43b0f79
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
107 lines
3.0 KiB
Nim
107 lines
3.0 KiB
Nim
## fmt.nim — Indentation-based Bux source formatter (bootstrap).
|
||
## Mirrors selfhost `src/fmt.bux`: re-indent by brace depth, preserve content.
|
||
|
||
import std/[strutils, os, algorithm]
|
||
|
||
proc isInStringOrComment(line: string, pos: int): bool =
|
||
## Simplified: track `//`, `"..."`, and `'...'` up to `pos`.
|
||
var inString = false
|
||
var inChar = false
|
||
var inComment = false
|
||
var i = 0
|
||
while i < pos and i < line.len:
|
||
let c = line[i]
|
||
let n = if i + 1 < line.len: line[i + 1] else: '\0'
|
||
if inComment:
|
||
inc i
|
||
continue
|
||
if c == '/' and n == '/':
|
||
inComment = true
|
||
inc i
|
||
continue
|
||
if c == '"' and not inChar:
|
||
inString = not inString
|
||
if c == '\'' and not inString:
|
||
inChar = not inChar
|
||
inc i
|
||
return inString or inChar or inComment
|
||
|
||
proc countBraceDelta(line: string): int =
|
||
var delta = 0
|
||
for i in 0 ..< line.len:
|
||
if isInStringOrComment(line, i):
|
||
continue
|
||
let c = line[i]
|
||
if c == '{':
|
||
inc delta
|
||
elif c == '}':
|
||
dec delta
|
||
return delta
|
||
|
||
proc formatSource*(source: string): string =
|
||
## Re-indent each non-empty line to 4 spaces × brace depth.
|
||
## Idempotent: formatting a clean file is a no-op.
|
||
var sb: string
|
||
var indent = 0
|
||
# Nim's splitLines leaves a trailing "" when the source ends with '\n'.
|
||
# Drop that artifact so we don't accumulate blank lines on re-format.
|
||
var lines = source.splitLines(keepEol = false)
|
||
if source.len > 0 and source.endsWith('\n') and lines.len > 0 and lines[^1].len == 0:
|
||
lines.setLen(lines.len - 1)
|
||
for line in lines:
|
||
let trimmed = line.strip(leading = true, trailing = false)
|
||
if trimmed.len == 0:
|
||
sb.add('\n')
|
||
continue
|
||
|
||
let delta = countBraceDelta(trimmed)
|
||
let firstChar = trimmed[0]
|
||
if firstChar == '}':
|
||
dec indent
|
||
if indent < 0:
|
||
indent = 0
|
||
|
||
for _ in 0 ..< indent:
|
||
sb.add(" ")
|
||
sb.add(trimmed)
|
||
sb.add('\n')
|
||
|
||
if firstChar != '}':
|
||
indent = indent + delta
|
||
else:
|
||
# Net delta after the initial decrease for a leading `}`
|
||
indent = indent + delta + 1
|
||
if indent < 0:
|
||
indent = 0
|
||
|
||
return sb
|
||
|
||
proc formatFile*(path: string, checkOnly: bool): tuple[ok: bool, changed: bool, msg: string] =
|
||
## Format `path` in place, or only check if reformatting would change it.
|
||
if not fileExists(path):
|
||
return (false, false, "file not found: " & path)
|
||
let source = readFile(path)
|
||
let formatted = formatSource(source)
|
||
if formatted == source:
|
||
return (true, false, "")
|
||
if checkOnly:
|
||
return (true, true, "would reformat")
|
||
try:
|
||
writeFile(path, formatted)
|
||
return (true, true, "formatted")
|
||
except CatchableError as e:
|
||
return (false, false, e.msg)
|
||
|
||
proc collectBuxFiles*(root: string): seq[string] =
|
||
## Collect `.bux` files: single file, or recursive directory walk.
|
||
result = @[]
|
||
if fileExists(root) and root.endsWith(".bux"):
|
||
result.add(root)
|
||
return
|
||
if not dirExists(root):
|
||
return
|
||
for path in walkDirRec(root):
|
||
if path.endsWith(".bux"):
|
||
result.add(path)
|
||
result.sort(system.cmp)
|