use ascii; use bufio; use encoding::utf8; use fs; use fmt; use io; use memio; use os; use strings; use unix::tty; def CONTROLS: str = "^S: save ^Q: quit"; def SCRATCH: str = "*scratch*"; def TABSTOP: u8 = 8; // Escape codes reference: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 def EC_ALTBUF_HIDE: str = "\x1b[?1049l"; def EC_ALTBUF_SHOW: str = "\x1b[?1049h"; def EC_CURSOR_HIDE: str = "\x1b[?25l"; def EC_CURSOR_HOME: str = "\x1b[H"; def EC_CURSOR_SHOW: str = "\x1b[?25h"; def EC_ERASE_LINE: str = "\x1b[K"; def EC_STYLE_DEFAULT: str = "\x1b[m"; def EC_STYLE_REVERSE: str = "\x1b[7m"; type key = enum u32 { SPACE = ' ', ESCAPE = '\x1b', MOD_CTRL = '\x1f', DELETE = '\x7f', // Unicode private use area ARROW_DOWN = '\uFF00', ARROW_LEFT, ARROW_RIGHT, ARROW_UP, END, HOME, PAGE_DOWN, PAGE_UP, }; type editor = struct { // TODO: This uses a ton of memory because runes are encoded as u32 buf: [][]rune, bufname: (str | void), cursor_x: size, cursor_y: size, dirty: bool, msg: str, offset_x: size, offset_y: size, screen_cols: size, screen_rows: size, termios: tty::termios, }; // TERMINAL fn terminal_read_key() u32 = { let buf: [4]u8 = [0...]; const read = io::read(os::stdin_file, &buf)! as size; if (read == 0) fmt::fatal("Failed to read from stdin"); const first = buf[0]; switch (first) { case '\x1b' => switch (buf[1]) { case '[' => switch (buf[2]) { case '5' => if (buf[3] == '~') return key::PAGE_UP; case '6' => if (buf[3] == '~') return key::PAGE_DOWN; case 'A' => return key::ARROW_UP; case 'B' => return key::ARROW_DOWN; case 'C' => return key::ARROW_RIGHT; case 'D' => return key::ARROW_LEFT; case 'F' => return key::END; case 'H' => return key::HOME; case => void; }; case => void; }; return '\x1b'; case => void; }; return first: u32; }; // ROW OPERATIONS fn editor_delete(e: *editor) void = { if (e.cursor_x > 0) { delete(e.buf[e.cursor_y][e.cursor_x - 1]); e.cursor_x -= 1; } else if (e.cursor_y > 0) { let previous_line = &e.buf[e.cursor_y - 1]; const previous_line_len = len(previous_line); append(previous_line, e.buf[e.cursor_y]...)!; delete(e.buf[e.cursor_y]); e.cursor_y -= 1; e.cursor_x = previous_line_len; }; e.dirty = true; }; fn editor_insert(e: *editor, k: rune) void = { insert(e.buf[e.cursor_y][e.cursor_x], k)!; e.cursor_x += 1; e.dirty = true; }; fn editor_newline(e: *editor) void = { let current_line = &e.buf[e.cursor_y]; const next_line_len = len(current_line) - e.cursor_x; const next_line = if (next_line_len > 0) { let next_line: []rune = alloc(['\0'...], next_line_len)!; next_line[..] = current_line[e.cursor_x..]; delete(e.buf[e.cursor_y][e.cursor_x..]); yield next_line; } else { yield []: []rune; }; insert(e.buf[e.cursor_y + 1], next_line)!; e.cursor_x = 0; e.cursor_y += 1; e.dirty = true; }; // FILE OPERATIONS fn editor_open(e: *editor, path: str) void = { editor_close(e); let file = os::open(path, fs::flag::RDONLY)!; defer io::close(file)!; let buf: [][]rune = []; let first: []rune = []; append(buf, first)!; let line = 0; let new_row = false; for (true) { match (bufio::read_rune(file)) { case utf8::invalid => fmt::fatal("File must be valid UTF-8"); case let err: io::error => fmt::fatal(io::strerror(err)); case let c: rune => if (new_row) { new_row = false; line += 1; let next_line: []rune = []; append(buf, next_line)!; }; switch (c) { case '\n' => new_row = true; case => append(buf[line], c)!; }; case io::EOF => break; }; }; e.buf = buf; e.bufname = path; e.dirty = false; }; fn editor_close(e: *editor) void = { for (let i = 0z; i < len(e.buf); i += 1) { free(e.buf[i]); }; free(e.buf); }; fn editor_save(e: *editor) void = { if (e.bufname is void) { e.msg = "Cannot save scratch buffer"; return; }; let file = os::open(e.bufname as str, fs::flag::TRUNC | fs::flag::WRONLY)!; defer io::close(file)!; for (let y = 0z; y < len(e.buf); y += 1) { const line = &e.buf[y]; // We can't use the spread operator here :( for (let x = 0z; x < len(line); x += 1) fmt::fprint(file, line[x])!; fmt::fprint(file, '\n')!; }; e.msg = "Saved to disk"; e.dirty = false; }; // OUTPUT fn editor_scroll(e: *editor) void = { if (e.cursor_x > len(e.buf[e.cursor_y])) e.cursor_x = len(e.buf[e.cursor_y]); if (e.cursor_x < e.offset_x) e.offset_x = e.cursor_x; if (e.cursor_x >= e.offset_x + e.screen_cols - 1) e.offset_x = e.cursor_x - e.screen_cols + 1; if (e.cursor_y < e.offset_y) e.offset_y = e.cursor_y; if (e.cursor_y >= e.offset_y + e.screen_rows - 1) e.offset_y = e.cursor_y - e.screen_rows + 2; }; fn editor_draw_line(e: *editor, out: *memio::stream, line: *[]rune) void = { if (e.offset_x > len(line)) return; let length = len(line) - e.offset_x; if (length > e.screen_cols) length = e.screen_cols; const end = e.offset_x + length; let dx: size = 0; for (let x = e.offset_x; x < end; x += 1) { const c = line[x]; switch (c) { case '\t' => for (let i = 0u8; i < TABSTOP; i += 1) { fmt::fprint(out, ' ')!; dx += 1; if (dx > e.screen_cols) break; }; case => fmt::fprint(out, c)!; dx += 1; }; if (dx > e.screen_cols) break; }; for (let x = dx; x < e.screen_cols; x += 1) fmt::fprint(out, key::SPACE: u32)!; }; fn editor_draw_screen(e: *editor) void = { // Update offset to keep cursor on the screen editor_scroll(e); // Construct contents let out = memio::dynamic(); defer io::close(&out)!; fmt::fprintf(&out, "{}{}", EC_CURSOR_HOME, EC_CURSOR_HIDE)!; // Buffer lines for (let y = 0z; y < e.screen_rows - 1; y += 1) { const disp_y = e.offset_y + y; if (disp_y >= len(e.buf)) { fmt::fprint(&out, "~")!; fmt::fprint(&out, EC_ERASE_LINE)!; } else { const line = e.buf[disp_y]; editor_draw_line(e, &out, &line); }; fmt::fprint(&out, "\r\n")!; }; // Status line // FIXME: Avoid heap allocation const state = fmt::asprintf( "{}:{}:{}", if (e.bufname is str) e.bufname as str else SCRATCH, e.cursor_x + 1, e.cursor_y + 1, )!; const msg = e.msg; let space_count = e.screen_cols - len(state) - len(msg) - len(CONTROLS) - 1; if (e.dirty) space_count -= 3; const spaces: []u8 = alloc([' '...], space_count)!; fmt::fprintf(&out, "{}{}", EC_STYLE_REVERSE, CONTROLS )!; io::write(&out, spaces)!; fmt::fprintf(&out, "{} {}{}{}", msg, if (e.dirty) "[*]" else "", state, EC_STYLE_DEFAULT )!; free(state); free(spaces); // Show and position cursor let cursor_x = { const line = e.buf[e.cursor_y]; let tabs = 0z; for (let x = e.offset_x; x < e.cursor_x; x += 1) { if (line[x] == '\t') tabs += 1; }; yield e.cursor_x - tabs + (tabs * TABSTOP: size); }; fmt::fprintf( &out, "\x1b[{};{}H", e.cursor_y - e.offset_y + 1, cursor_x + 1 )!; fmt::fprint(&out, EC_CURSOR_SHOW)!; // Commit to terminal io::write(os::stdout_file, memio::buffer(&out))!; }; // INPUT fn editor_move_cursor(e: *editor, k: u32) void = { switch (k) { case key::ARROW_DOWN => if (e.cursor_y < len(e.buf) - 1) e.cursor_y += 1; case key::ARROW_LEFT => if (e.cursor_x > 0) { e.cursor_x -= 1; } else if (e.cursor_y > 0) { e.cursor_y -= 1; e.cursor_x = len(e.buf[e.cursor_y]); }; case key::ARROW_RIGHT => const line = e.buf[e.cursor_y]; if (e.cursor_x < len(line)) { e.cursor_x += 1; } else if (e.cursor_y < len(e.buf) - 1) { e.cursor_y += 1; e.cursor_x = 0; }; case key::ARROW_UP => if (e.cursor_y > 0) e.cursor_y -= 1; case => void; }; }; // Returns `true` if the program should exit fn editor_process_key(e: *editor) bool = { const k = terminal_read_key(); switch (k) { case key::ARROW_DOWN, key::ARROW_LEFT, key::ARROW_RIGHT, key::ARROW_UP => editor_move_cursor(e, k); case key::HOME => e.cursor_x = 0; case key::END => e.cursor_x = len(e.buf[e.cursor_y]); case key::PAGE_DOWN => e.cursor_y = e.offset_y + e.screen_rows - 2; if (e.cursor_y > len(e.buf) - 1) e.cursor_y = len(e.buf) - 1; for (let i = 0z; i < e.screen_rows - 1; i += 1) editor_move_cursor(e, key::ARROW_DOWN); case key::PAGE_UP => e.cursor_y = e.offset_y; for (let i = 0z; i < e.screen_rows - 1; i += 1) editor_move_cursor(e, key::ARROW_UP); case 'q' & key::MOD_CTRL => return true; case 's' & key::MOD_CTRL => editor_save(e); case '\b', key::DELETE => editor_delete(e); case '\r' => editor_newline(e); case => if (k == '\t' || ascii::isprint(k: rune)) editor_insert(e, k: rune); }; return false; }; // INIT fn editor_init() editor = { let termios = tty::termios_query(os::stdin_file)!; fmt::fprint(os::stdout_file, EC_ALTBUF_SHOW)!; tty::makeraw(&termios)!; let buf: [][]rune = []; // Invariant: There is always at least one line in a buffer append(buf, [])!; const winsize = tty::winsize(os::stdout_file)!; return editor { buf = buf, bufname = void, screen_cols = winsize.columns: size, screen_rows = winsize.rows: size, termios = termios, ... }; }; fn editor_finish(e: editor) void = { editor_close(&e); fmt::fprint(os::stdout_file, EC_ALTBUF_HIDE)!; tty::termios_restore(&e.termios); }; export fn main() void = { let e = editor_init(); defer editor_finish(e); if (len(os::args) > 1) editor_open(&e, os::args[1]); editor_draw_screen(&e); for (true) { e.msg = ""; if (editor_process_key(&e)) break; editor_draw_screen(&e); }; };