#![allow(non_snake_case)]
#![allow(dead_code)]
use std::io::{self, Write};
use crate::ported::crt::{
ColorElements, ColorScheme, KEY_BACKSPACE, KEY_CTRL, KEY_DC, KEY_END, KEY_HOME, KEY_LEFT,
KEY_RIGHT, KEY_SLEFT, KEY_SRIGHT,
};
use crate::ported::functionbar::Ncurses;
pub const LINEEDITOR_MAX: usize = 128;
pub struct LineEditor {
buffer: [u8; LINEEDITOR_MAX + 1],
len: usize,
cursor: usize,
scroll: usize,
maxLen: usize,
}
impl Default for LineEditor {
fn default() -> Self {
LineEditor {
buffer: [0; LINEEDITOR_MAX + 1],
len: 0,
cursor: 0,
scroll: 0,
maxLen: 0,
}
}
}
pub fn LineEditor_init(this: &mut LineEditor) {
LineEditor_initWithMax(this, LINEEDITOR_MAX);
}
pub fn LineEditor_initWithMax(this: &mut LineEditor, maxLen: usize) {
this.buffer[0] = b'\0';
this.len = 0;
this.cursor = 0;
this.scroll = 0;
this.maxLen = if maxLen > 0 && maxLen <= LINEEDITOR_MAX {
maxLen
} else {
LINEEDITOR_MAX
};
}
pub fn LineEditor_reset(this: &mut LineEditor) {
this.buffer[0] = b'\0';
this.len = 0;
this.cursor = 0;
this.scroll = 0;
}
pub fn LineEditor_setText(this: &mut LineEditor, text: &str) {
let copyLen = this.maxLen;
let src = text.as_bytes();
let srcEnd = src.iter().position(|&b| b == 0).unwrap_or(src.len());
for i in 0..copyLen {
this.buffer[i] = if i < srcEnd { src[i] } else { 0 };
}
this.buffer[copyLen] = b'\0';
this.len = this.buffer[..this.maxLen]
.iter()
.position(|&b| b == 0)
.unwrap_or(this.maxLen);
this.cursor = this.len;
this.scroll = 0;
}
pub fn LineEditor_getText(this: &LineEditor) -> &str {
std::str::from_utf8(&this.buffer[..this.len]).unwrap_or("")
}
pub fn LineEditor_getCursor(this: &LineEditor) -> usize {
this.cursor
}
pub fn moveCursorLeft(this: &mut LineEditor) {
if this.cursor > 0 {
this.cursor -= 1;
}
}
pub fn moveCursorRight(this: &mut LineEditor) {
if this.cursor < this.len {
this.cursor += 1;
}
}
pub fn moveCursorWordLeft(this: &mut LineEditor) {
let mut pos = this.cursor;
while pos > 0
&& matches!(
this.buffer[pos - 1],
b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'
)
{
pos -= 1;
}
while pos > 0
&& !matches!(
this.buffer[pos - 1],
b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'
)
{
pos -= 1;
}
this.cursor = pos;
}
pub fn moveCursorWordRight(this: &mut LineEditor) {
let mut pos = this.cursor;
let len = this.len;
while pos < len && !matches!(this.buffer[pos], b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r') {
pos += 1;
}
while pos < len && matches!(this.buffer[pos], b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r') {
pos += 1;
}
this.cursor = pos;
}
pub fn deleteCharBefore(this: &mut LineEditor) -> bool {
if this.cursor == 0 {
return false;
}
let pos = this.cursor - 1;
this.buffer.copy_within(this.cursor..this.len + 1, pos);
this.len -= 1;
this.cursor = pos;
true
}
pub fn deleteCharAt(this: &mut LineEditor) -> bool {
if this.cursor >= this.len {
return false;
}
this.buffer
.copy_within(this.cursor + 1..this.len + 1, this.cursor);
this.len -= 1;
true
}
pub fn insertChar(this: &mut LineEditor, ch: u8) -> bool {
if this.len >= this.maxLen {
return false;
}
this.buffer
.copy_within(this.cursor..this.len + 1, this.cursor + 1);
this.buffer[this.cursor] = ch;
this.cursor += 1;
this.len += 1;
true
}
const LE_CTRL_B: i32 = KEY_CTRL(b'B' as i32);
const LE_CTRL_F: i32 = KEY_CTRL(b'F' as i32);
const LE_CTRL_A: i32 = KEY_CTRL(b'A' as i32);
const LE_CTRL_E: i32 = KEY_CTRL(b'E' as i32);
const LE_CTRL_W: i32 = KEY_CTRL(b'W' as i32);
const LE_CTRL_K: i32 = KEY_CTRL(b'K' as i32);
const LE_CTRL_U: i32 = KEY_CTRL(b'U' as i32);
pub fn LineEditor_handleKey(this: &mut LineEditor, ch: i32) -> bool {
match ch {
KEY_LEFT | LE_CTRL_B => {
moveCursorLeft(this);
false
}
KEY_RIGHT | LE_CTRL_F => {
moveCursorRight(this);
false
}
KEY_HOME | LE_CTRL_A => {
this.cursor = 0;
false
}
KEY_END | LE_CTRL_E => {
this.cursor = this.len;
false
}
KEY_SLEFT => {
moveCursorWordLeft(this);
false
}
KEY_SRIGHT => {
moveCursorWordRight(this);
false
}
KEY_DC => deleteCharAt(this),
KEY_BACKSPACE | 127 => deleteCharBefore(this),
LE_CTRL_W => {
let end = this.cursor;
while this.cursor > 0
&& matches!(
this.buffer[this.cursor - 1],
b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'
)
{
this.cursor -= 1;
}
while this.cursor > 0
&& !matches!(
this.buffer[this.cursor - 1],
b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r'
)
{
this.cursor -= 1;
}
if this.cursor == end {
return false;
}
let deleted = end - this.cursor;
this.buffer.copy_within(end..this.len + 1, this.cursor);
this.len -= deleted;
true
}
LE_CTRL_K => {
if this.cursor >= this.len {
return false;
}
this.buffer[this.cursor] = b'\0';
this.len = this.cursor;
true
}
LE_CTRL_U => {
if this.cursor == 0 {
return false;
}
this.buffer.copy_within(this.cursor..this.len + 1, 0);
this.len -= this.cursor;
this.cursor = 0;
true
}
_ => {
if ch > 0 && ch < 256 && matches!(ch as u8, 0x20..=0x7e) {
insertChar(this, ch as u8)
} else {
false
}
}
}
}
pub fn LineEditor_updateScroll(this: &mut LineEditor, fieldWidth: i32) {
if fieldWidth <= 0 {
return;
}
let fw = fieldWidth as usize;
if this.cursor < this.scroll {
this.scroll = this.cursor;
} else if this.cursor >= this.scroll + fw {
this.scroll = this.cursor - fw + 1;
}
}
pub fn LineEditor_draw(this: &LineEditor, startX: i32, fieldWidth: i32, attr: i32) -> i32 {
let mut out = io::stdout().lock();
if attr == -1 {
Ncurses::attrset(
&mut out,
ColorElements::FUNCTION_BAR.packed(ColorScheme::active()),
);
} else {
Ncurses::attrset(&mut out, attr);
}
let line = Ncurses::lines() - 1;
let mut visibleLen = this.len as i32 - this.scroll as i32;
if visibleLen < 0 {
visibleLen = 0;
}
if visibleLen > fieldWidth {
visibleLen = fieldWidth;
}
let end = this.scroll + visibleLen as usize;
let visibleStart = std::str::from_utf8(&this.buffer[this.scroll..end]).unwrap_or("");
Ncurses::mvaddnstr(&mut out, line, startX, visibleStart, visibleLen);
for i in visibleLen..fieldWidth {
Ncurses::mvaddch(&mut out, line, startX + i, ' ');
}
let _ = out.flush();
startX + (this.cursor as i32 - this.scroll as i32)
}
pub fn LineEditor_click(this: &mut LineEditor, clickX: i32, fieldStartX: i32) {
let mut offset = clickX - fieldStartX;
if offset < 0 {
offset = 0;
}
let mut newCursor = this.scroll + offset as usize;
if newCursor > this.len {
newCursor = this.len;
}
this.cursor = newCursor;
}
#[cfg(test)]
mod tests {
use super::*;
fn text(e: &LineEditor) -> String {
String::from_utf8_lossy(&e.buffer[..e.len]).into_owned()
}
#[test]
fn init_and_initWithMax() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
assert_eq!(e.maxLen, LINEEDITOR_MAX);
assert_eq!(e.len, 0);
assert_eq!(e.cursor, 0);
assert_eq!(e.scroll, 0);
assert_eq!(e.buffer[0], b'\0');
LineEditor_initWithMax(&mut e, 10);
assert_eq!(e.maxLen, 10);
LineEditor_initWithMax(&mut e, 0);
assert_eq!(e.maxLen, LINEEDITOR_MAX);
LineEditor_initWithMax(&mut e, LINEEDITOR_MAX + 5);
assert_eq!(e.maxLen, LINEEDITOR_MAX);
}
#[test]
fn reset_clears_state() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "hello");
e.scroll = 3;
LineEditor_reset(&mut e);
assert_eq!(e.len, 0);
assert_eq!(e.cursor, 0);
assert_eq!(e.scroll, 0);
assert_eq!(e.buffer[0], b'\0');
}
#[test]
fn setText_puts_cursor_at_end() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "hello");
assert_eq!(text(&e), "hello");
assert_eq!(e.len, 5);
assert_eq!(e.cursor, 5);
assert_eq!(e.scroll, 0);
}
#[test]
fn setText_truncates_to_maxLen() {
let mut e = LineEditor::default();
LineEditor_initWithMax(&mut e, 3);
LineEditor_setText(&mut e, "abcdef");
assert_eq!(text(&e), "abc");
assert_eq!(e.len, 3);
assert_eq!(e.cursor, 3);
}
#[test]
fn moveCursor_left_right_bounds() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "ab"); moveCursorRight(&mut e); assert_eq!(e.cursor, 2);
moveCursorLeft(&mut e);
assert_eq!(e.cursor, 1);
moveCursorLeft(&mut e);
assert_eq!(e.cursor, 0);
moveCursorLeft(&mut e); assert_eq!(e.cursor, 0);
moveCursorRight(&mut e);
assert_eq!(e.cursor, 1);
}
#[test]
fn moveCursorWordLeft_jumps_over_spaces_and_word() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "foo bar"); moveCursorWordLeft(&mut e); assert_eq!(e.cursor, 4);
moveCursorWordLeft(&mut e); assert_eq!(e.cursor, 0);
moveCursorWordLeft(&mut e); assert_eq!(e.cursor, 0);
}
#[test]
fn moveCursorWordLeft_from_within_trailing_spaces() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "ab "); moveCursorWordLeft(&mut e); assert_eq!(e.cursor, 0);
}
#[test]
fn moveCursorWordRight_jumps_over_word_and_spaces() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "foo bar");
e.cursor = 0;
moveCursorWordRight(&mut e); assert_eq!(e.cursor, 4);
moveCursorWordRight(&mut e); assert_eq!(e.cursor, 7);
moveCursorWordRight(&mut e); assert_eq!(e.cursor, 7);
}
#[test]
fn deleteCharBefore_at_start_is_noop() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "ab");
e.cursor = 0;
assert!(!deleteCharBefore(&mut e));
assert_eq!(text(&e), "ab");
assert_eq!(e.len, 2);
}
#[test]
fn deleteCharBefore_removes_and_moves_cursor() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc"); assert!(deleteCharBefore(&mut e));
assert_eq!(text(&e), "ab");
assert_eq!(e.len, 2);
assert_eq!(e.cursor, 2);
assert_eq!(e.buffer[e.len], b'\0'); }
#[test]
fn deleteCharBefore_in_middle() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc");
e.cursor = 1; assert!(deleteCharBefore(&mut e)); assert_eq!(text(&e), "bc");
assert_eq!(e.cursor, 0);
}
#[test]
fn deleteCharAt_at_end_is_noop() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "ab"); assert!(!deleteCharAt(&mut e));
assert_eq!(text(&e), "ab");
}
#[test]
fn deleteCharAt_removes_current() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc");
e.cursor = 1;
assert!(deleteCharAt(&mut e)); assert_eq!(text(&e), "ac");
assert_eq!(e.len, 2);
assert_eq!(e.cursor, 1); assert_eq!(e.buffer[e.len], b'\0');
}
#[test]
fn insertChar_in_middle() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "ac");
e.cursor = 1;
assert!(insertChar(&mut e, b'b'));
assert_eq!(text(&e), "abc");
assert_eq!(e.len, 3);
assert_eq!(e.cursor, 2);
assert_eq!(e.buffer[e.len], b'\0'); }
#[test]
fn insertChar_at_maxLen_is_rejected() {
let mut e = LineEditor::default();
LineEditor_initWithMax(&mut e, 3);
LineEditor_setText(&mut e, "abc"); assert!(!insertChar(&mut e, b'd'));
assert_eq!(text(&e), "abc");
assert_eq!(e.len, 3);
}
#[test]
fn insertChar_up_to_maxLen_boundary() {
let mut e = LineEditor::default();
LineEditor_initWithMax(&mut e, 3);
LineEditor_setText(&mut e, "ab"); assert!(insertChar(&mut e, b'c')); assert_eq!(text(&e), "abc");
assert!(!insertChar(&mut e, b'd')); assert_eq!(e.len, 3);
}
#[test]
fn updateScroll_noop_on_nonpositive_width() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
e.cursor = 10;
e.scroll = 4;
LineEditor_updateScroll(&mut e, 0);
assert_eq!(e.scroll, 4);
LineEditor_updateScroll(&mut e, -1);
assert_eq!(e.scroll, 4);
}
#[test]
fn updateScroll_scrolls_right_when_cursor_past_window() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
e.cursor = 10;
e.scroll = 0;
LineEditor_updateScroll(&mut e, 5); assert_eq!(e.scroll, 6); }
#[test]
fn updateScroll_scrolls_left_when_cursor_before_window() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
e.cursor = 3;
e.scroll = 6;
LineEditor_updateScroll(&mut e, 5); assert_eq!(e.scroll, 3);
}
#[test]
fn updateScroll_no_change_when_cursor_visible() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
e.cursor = 4;
e.scroll = 2;
LineEditor_updateScroll(&mut e, 5); assert_eq!(e.scroll, 2);
}
#[test]
fn getText_returns_buffer_text() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
assert_eq!(LineEditor_getText(&e), "");
LineEditor_setText(&mut e, "hello world");
assert_eq!(LineEditor_getText(&e), "hello world");
e.cursor = e.len;
assert!(deleteCharBefore(&mut e));
assert_eq!(LineEditor_getText(&e), "hello worl");
}
#[test]
fn handleKey_printable_inserts_and_advances_cursor() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
assert!(LineEditor_handleKey(&mut e, b'a' as i32));
assert!(LineEditor_handleKey(&mut e, b'b' as i32));
assert_eq!(text(&e), "ab");
assert_eq!(e.len, 2);
assert_eq!(e.cursor, 2); assert!(!LineEditor_handleKey(&mut e, 0x01_000)); assert_eq!(text(&e), "ab");
}
#[test]
fn handleKey_backspace_deletes_before_cursor() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc"); assert!(LineEditor_handleKey(&mut e, KEY_BACKSPACE));
assert_eq!(text(&e), "ab");
assert_eq!(e.cursor, 2);
assert!(LineEditor_handleKey(&mut e, 127));
assert_eq!(text(&e), "a");
e.cursor = 0;
assert!(!LineEditor_handleKey(&mut e, KEY_BACKSPACE));
assert_eq!(text(&e), "a");
}
#[test]
fn handleKey_delete_removes_char_at_cursor() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc");
e.cursor = 1;
assert!(LineEditor_handleKey(&mut e, KEY_DC)); assert_eq!(text(&e), "ac");
assert_eq!(e.cursor, 1);
}
#[test]
fn handleKey_arrows_move_cursor_without_changing_text() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abc"); assert!(!LineEditor_handleKey(&mut e, KEY_LEFT)); assert_eq!(e.cursor, 2);
assert!(!LineEditor_handleKey(&mut e, KEY_LEFT));
assert_eq!(e.cursor, 1);
assert!(!LineEditor_handleKey(&mut e, KEY_RIGHT));
assert_eq!(e.cursor, 2);
assert!(!LineEditor_handleKey(&mut e, KEY_HOME));
assert_eq!(e.cursor, 0);
assert!(!LineEditor_handleKey(&mut e, KEY_END));
assert_eq!(e.cursor, 3);
assert_eq!(text(&e), "abc"); }
#[test]
fn handleKey_ctrl_w_deletes_word_before_cursor() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "foo bar"); assert!(LineEditor_handleKey(&mut e, KEY_CTRL(b'W' as i32)));
assert_eq!(text(&e), "foo ");
assert_eq!(e.cursor, 4);
}
#[test]
fn getCursor_reads_cursor_position() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
assert_eq!(LineEditor_getCursor(&e), 0);
LineEditor_setText(&mut e, "hello"); assert_eq!(LineEditor_getCursor(&e), 5);
moveCursorWordLeft(&mut e); assert_eq!(LineEditor_getCursor(&e), 0);
}
#[test]
fn click_maps_column_to_cursor_with_scroll() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abcdef"); e.scroll = 0;
LineEditor_click(&mut e, 3, 0);
assert_eq!(e.cursor, 3);
e.scroll = 2;
LineEditor_click(&mut e, 1, 5); assert_eq!(e.cursor, 2); LineEditor_click(&mut e, 8, 5); assert_eq!(e.cursor, 5);
LineEditor_click(&mut e, 100, 5);
assert_eq!(e.cursor, 6); }
#[test]
fn draw_returns_cursor_screen_column() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "hello"); e.scroll = 0;
let cursor_x = LineEditor_draw(&e, 10, 20, -1);
assert_eq!(cursor_x, 15);
e.scroll = 2;
let cursor_x = LineEditor_draw(&e, 10, 20, 0);
assert_eq!(cursor_x, 13); }
#[test]
fn handleKey_ctrl_k_and_ctrl_u_kill_line_regions() {
let mut e = LineEditor::default();
LineEditor_init(&mut e);
LineEditor_setText(&mut e, "abcdef");
e.cursor = 3;
assert!(LineEditor_handleKey(&mut e, KEY_CTRL(b'K' as i32)));
assert_eq!(text(&e), "abc");
assert_eq!(e.len, 3);
assert!(LineEditor_handleKey(&mut e, KEY_CTRL(b'U' as i32)));
assert_eq!(text(&e), "");
assert_eq!(e.cursor, 0);
}
}