///|
/// A single patch operation: move to (row, col) and replace the line with text.
pub(all) struct PatchOp {
row : Int
col : Int
text : String
} derive(Debug, Eq)
///|
fn next_line_end(view : String, start : Int) -> Int {
let n = view.length()
let mut i = start
while i < n && view[i] != '\n' {
i = i + 1
}
i
}
///|
/// Compare two view strings line-by-line and return the minimal set of
/// `PatchOp`s needed to transform the old view into the new view.
///
/// - Changed or added lines → emit a `PatchOp` with the new line text.
/// - Lines present in `old_view` but absent in `new_view` → emit a `PatchOp`
/// with `text = ""` to clear the stale line.
pub fn diff_views(old_view : String, new_view : String) -> Array[PatchOp] {
let ops : Array[PatchOp] = []
let old_len = old_view.length()
let new_len = new_view.length()
let mut old_start = 0
let mut new_start = 0
let mut row = 1
while old_start <= old_len || new_start <= new_len {
let old_has_line = old_start <= old_len
let new_has_line = new_start <= new_len
if old_has_line && new_has_line {
let old_end = next_line_end(old_view, old_start)
let new_end = next_line_end(new_view, new_start)
let old_line = old_view[old_start:old_end]
let new_line = new_view[new_start:new_end]
if old_line != new_line {
ops.push(PatchOp::{ row, col: 1, text: new_line.to_owned() })
}
old_start = old_end + 1
new_start = new_end + 1
} else if new_has_line {
let new_end = next_line_end(new_view, new_start)
let new_line = new_view[new_start:new_end]
if new_line != "" {
ops.push(PatchOp::{ row, col: 1, text: new_line.to_owned() })
}
new_start = new_end + 1
} else {
let old_end = next_line_end(old_view, old_start)
let old_line = old_view[old_start:old_end]
if old_line != "" {
ops.push(PatchOp::{ row, col: 1, text: "" })
}
old_start = old_end + 1
}
row = row + 1
}
ops
}
///|
/// Convert a list of `PatchOp`s into a single ANSI escape string that, when
/// written to the terminal, applies all the patches in order.
///
/// Each op moves the cursor to `(row, col)`, clears to end-of-line, then
/// writes `text`. Returns `""` when `ops` is empty.
pub fn render_patch(ops : Array[PatchOp]) -> String {
if ops.is_empty() {
return ""
}
let buf = StringBuilder::new()
for op in ops {
buf.write_string(move_cursor(op.row, op.col))
buf.write_string(clear_line())
buf.write_string(op.text)
}
buf.to_string()
}
///|
/// Render a complete view string to the terminal using cursor positioning.
///
/// Splits `view` on newlines and emits `move_cursor + clear_line + text` for
/// every line, then clears any remaining lines below down to `prev_lines`.
/// This avoids writing raw `\n` bytes in raw terminal mode.
pub fn render_full(view : String, prev_lines : Int) -> String {
let lines = split_lines(view)
let buf = StringBuilder::new()
for i in 0..