Initial commit: omp-edit DSH plugin (oh-my-pi port) with docs and tests
This commit is contained in:
commit
71953ffb8f
19 files changed
+2412
No files matched your search
+78
@@ -0,0 +1,78 @@
|
||||
// Text helpers shared by every edit mode: line splitting that remembers the
|
||||
// trailing newline, snapshot tags, and compact numbered previews.
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
/** Normalize CRLF/CR to LF. */
|
||||
export const toLF = (s) => String(s).replace(/\r\n?/g, '\n')
|
||||
|
||||
/** Split LF text into lines plus whether it ended with a newline. Empty text has no lines. */
|
||||
export function splitLines(text) {
|
||||
const t = toLF(text)
|
||||
if (t === '') return { lines: [], eol: false }
|
||||
const eol = t.endsWith('\n')
|
||||
const lines = (eol ? t.slice(0, -1) : t).split('\n')
|
||||
return { lines, eol }
|
||||
}
|
||||
|
||||
/** Inverse of splitLines. A file that gained content keeps (or adopts) a trailing newline. */
|
||||
export function joinLines(lines, eol) {
|
||||
if (lines.length === 0) return ''
|
||||
return lines.join('\n') + (eol ? '\n' : '')
|
||||
}
|
||||
|
||||
/** Dominant line ending of the original file, so writes preserve CRLF files. */
|
||||
export function detectEol(raw) {
|
||||
const crlf = (raw.match(/\r\n/g) || []).length
|
||||
const lf = (raw.match(/\n/g) || []).length - crlf
|
||||
return crlf > lf ? '\r\n' : '\n'
|
||||
}
|
||||
export const restoreEol = (lfText, eol) => (eol === '\r\n' ? lfText.replace(/\n/g, '\r\n') : lfText)
|
||||
|
||||
/** Four uppercase hex characters derived from the whole LF-normalized file. */
|
||||
export function fileTag(text) {
|
||||
return crypto.createHash('sha256').update(toLF(text)).digest('hex').slice(0, 4).toUpperCase()
|
||||
}
|
||||
|
||||
export const HEADER_RE = /^\[(.+?)#([0-9A-Fa-f]{4})\]\s*$/
|
||||
|
||||
export const formatHeader = (path, tag) => `[${path}#${tag}]`
|
||||
export const numbered = (n, text) => `${n}:${text}`
|
||||
|
||||
/**
|
||||
* Compact post-edit preview: changed regions of the new text (by line numbers)
|
||||
* with `context` lines around them, separated by `…`.
|
||||
* @param {string[]} lines new file lines
|
||||
* @param {Array<[number, number]>} regions 1-based inclusive [start, end] in new numbering
|
||||
*/
|
||||
export function preview(lines, regions, { context = 2, maxLines = 80 } = {}) {
|
||||
if (!regions.length || !lines.length) return []
|
||||
const spans = regions
|
||||
.map(([a, b]) => [Math.max(1, a - context), Math.min(lines.length, Math.max(a, b) + context)])
|
||||
.sort((x, y) => x[0] - y[0])
|
||||
const merged = []
|
||||
for (const s of spans) {
|
||||
const last = merged.at(-1)
|
||||
if (last && s[0] <= last[1] + 1) last[1] = Math.max(last[1], s[1])
|
||||
else merged.push([...s])
|
||||
}
|
||||
const out = []
|
||||
for (const [a, b] of merged) {
|
||||
if (out.length) out.push('…')
|
||||
for (let n = a; n <= b; n++) {
|
||||
if (out.length >= maxLines) { out.push('… (preview truncated; read the file for more)'); return out }
|
||||
out.push(numbered(n, lines[n - 1]))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Error type that carries a model-facing message. */
|
||||
export class EditError extends Error {
|
||||
constructor(message, extra = {}) {
|
||||
super(message)
|
||||
this.name = 'EditError'
|
||||
Object.assign(this, extra)
|
||||
}
|
||||
}
|
||||
|
||||
export const jsonQuote = (s) => JSON.stringify(String(s))
|
||||
Reference in new issue
Block a user