Files
bare-operating-system/packages/bare-os-coreutils/lib/edit-buffer.js
T
2026-04-03 21:59:05 -04:00

236 lines
6.3 KiB
JavaScript

/** Text buffer + cursor + undo for /bin/edit. */
const EDIT_UNDO_MAX = 200
/**
* @param {string} text
*/
function bareEditCreateBuffer(text) {
const lines =
text === '' ? [''] : String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
return {
lines,
row: 0,
col: 0,
dirty: false,
/** @type {{ lines: string[], row: number, col: number }[]} */
undo: [],
/** @type {{ lines: string[], row: number, col: number }[]} */
redo: []
}
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditSnapshot(buf) {
return {
lines: buf.lines.slice(),
row: buf.row,
col: buf.col
}
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditPushUndo(buf) {
buf.undo.push(bareEditSnapshot(buf))
if (buf.undo.length > EDIT_UNDO_MAX) buf.undo.shift()
buf.redo = []
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditUndo(buf) {
const prev = buf.undo.pop()
if (!prev) return false
buf.redo.push(bareEditSnapshot(buf))
buf.lines = /** @type {{ lines: string[] }} */ (prev).lines.slice()
buf.row = prev.row
buf.col = prev.col
buf.dirty = true
return true
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditRedo(buf) {
const next = buf.redo.pop()
if (!next) return false
buf.undo.push(bareEditSnapshot(buf))
buf.lines = /** @type {{ lines: string[] }} */ (next).lines.slice()
buf.row = next.row
buf.col = next.col
buf.dirty = true
return true
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditClampCursor(buf) {
if (buf.row < 0) buf.row = 0
if (buf.row >= buf.lines.length) buf.row = buf.lines.length - 1
const line = buf.lines[buf.row] || ''
if (buf.col < 0) buf.col = 0
if (buf.col > line.length) buf.col = line.length
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
* @param {string} ch single code unit (MVP)
*/
function bareEditInsertChar(buf, ch) {
bareEditPushUndo(buf)
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col) + ch + line.slice(buf.col)
buf.col += ch.length
buf.dirty = true
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditNewline(buf) {
bareEditPushUndo(buf)
const line = buf.lines[buf.row]
const rest = line.slice(buf.col)
buf.lines[buf.row] = line.slice(0, buf.col)
buf.lines.splice(buf.row + 1, 0, rest)
buf.row++
buf.col = 0
buf.dirty = true
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditBackspace(buf) {
if (buf.col > 0) {
bareEditPushUndo(buf)
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col - 1) + line.slice(buf.col)
buf.col--
buf.dirty = true
return
}
if (buf.row > 0) {
bareEditPushUndo(buf)
const prevLen = buf.lines[buf.row - 1].length
buf.lines[buf.row - 1] += buf.lines[buf.row]
buf.lines.splice(buf.row, 1)
buf.row--
buf.col = prevLen
buf.dirty = true
}
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditDelete(buf) {
const line = buf.lines[buf.row]
if (buf.col < line.length) {
bareEditPushUndo(buf)
buf.lines[buf.row] = line.slice(0, buf.col) + line.slice(buf.col + 1)
buf.dirty = true
return
}
if (buf.row < buf.lines.length - 1) {
bareEditPushUndo(buf)
buf.lines[buf.row] += buf.lines[buf.row + 1]
buf.lines.splice(buf.row + 1, 1)
buf.dirty = true
}
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
* @param {'home'|'end'|'up'|'down'|'left'|'right'} key
*/
function bareEditMoveKey(buf, key) {
if (key === 'home') {
buf.col = 0
return
}
if (key === 'end') {
buf.col = buf.lines[buf.row].length
return
}
if (key === 'up') {
if (buf.row > 0) {
buf.row--
buf.col = Math.min(buf.col, buf.lines[buf.row].length)
}
return
}
if (key === 'down') {
if (buf.row < buf.lines.length - 1) {
buf.row++
buf.col = Math.min(buf.col, buf.lines[buf.row].length)
}
return
}
if (key === 'left') {
if (buf.col > 0) buf.col--
else if (buf.row > 0) {
buf.row--
buf.col = buf.lines[buf.row].length
}
return
}
if (key === 'right') {
const line = buf.lines[buf.row]
if (buf.col < line.length) buf.col++
else if (buf.row < buf.lines.length - 1) {
buf.row++
buf.col = 0
}
}
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
function bareEditJoinAll(buf) {
return buf.lines.join('\n')
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
* @param {string} needle
* @param {number} fromRow
* @param {number} fromCol
* @returns {{ row: number, col: number } | null}
*/
function bareEditFindNext(buf, needle, fromRow, fromCol) {
if (!needle) return null
for (let r = fromRow; r < buf.lines.length; r++) {
const line = buf.lines[r] || ''
const start = r === fromRow ? fromCol : 0
const idx = line.indexOf(needle, start)
if (idx >= 0) return { row: r, col: idx }
}
for (let r = 0; r < fromRow; r++) {
const idx = (buf.lines[r] || '').indexOf(needle)
if (idx >= 0) return { row: r, col: idx }
}
const line = buf.lines[fromRow] || ''
const idx = line.indexOf(needle, 0)
if (idx >= 0 && idx < fromCol) return { row: fromRow, col: idx }
return null
}
/**
* @param {{ lines: string[], row: number, col: number }} buf
* @param {number} targetRow 1-based
*/
function bareEditGotoLine(buf, targetRow) {
const r = Math.max(1, Math.min(buf.lines.length, targetRow)) - 1
buf.row = r
buf.col = Math.min(buf.col, buf.lines[r].length)
}