236 lines
6.3 KiB
JavaScript
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)
|
|
}
|