z_mirror/hare-kilo
git clone https://git.raiguard.me/z_mirror/hare-kilo
caef1ef
trunk
1use ascii; 2use bufio; 3use encoding::utf8; 4use fs; 5use fmt; 6use io; 7use memio; 8use os; 9use strings; 10use unix::tty; 11 12def CONTROLS: str = "^S: save ^Q: quit"; 13def SCRATCH: str = "*scratch*"; 14def TABSTOP: u8 = 8; 15 16// Escape codes reference: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 17def EC_ALTBUF_HIDE: str = "\x1b[?1049l"; 18def EC_ALTBUF_SHOW: str = "\x1b[?1049h"; 19def EC_CURSOR_HIDE: str = "\x1b[?25l"; 20def EC_CURSOR_HOME: str = "\x1b[H"; 21def EC_CURSOR_SHOW: str = "\x1b[?25h"; 22def EC_ERASE_LINE: str = "\x1b[K"; 23def EC_STYLE_DEFAULT: str = "\x1b[m"; 24def EC_STYLE_REVERSE: str = "\x1b[7m"; 25 26type key = enum u32 { 27 SPACE = ' ', 28 ESCAPE = '\x1b', 29 MOD_CTRL = '\x1f', 30 DELETE = '\x7f', 31 // Unicode private use area 32 ARROW_DOWN = '\uFF00', 33 ARROW_LEFT, 34 ARROW_RIGHT, 35 ARROW_UP, 36 END, 37 HOME, 38 PAGE_DOWN, 39 PAGE_UP, 40}; 41 42type editor = struct { 43 // TODO: This uses a ton of memory because runes are encoded as u32 44 buf: [][]rune, 45 bufname: (str | void), 46 cursor_x: size, 47 cursor_y: size, 48 dirty: bool, 49 msg: str, 50 offset_x: size, 51 offset_y: size, 52 screen_cols: size, 53 screen_rows: size, 54 termios: tty::termios, 55}; 56 57// TERMINAL 58 59fn terminal_read_key() u32 = { 60 let buf: [4]u8 = [0...]; 61 const read = io::read(os::stdin_file, &buf)! as size; 62 if (read == 0) 63 fmt::fatal("Failed to read from stdin"); 64 const first = buf[0]; 65 switch (first) { 66 case '\x1b' => 67 switch (buf[1]) { 68 case '[' => 69 switch (buf[2]) { 70 case '5' => 71 if (buf[3] == '~') 72 return key::PAGE_UP; 73 case '6' => 74 if (buf[3] == '~') 75 return key::PAGE_DOWN; 76 case 'A' => 77 return key::ARROW_UP; 78 case 'B' => 79 return key::ARROW_DOWN; 80 case 'C' => 81 return key::ARROW_RIGHT; 82 case 'D' => 83 return key::ARROW_LEFT; 84 case 'F' => 85 return key::END; 86 case 'H' => 87 return key::HOME; 88 case => void; 89 }; 90 case => void; 91 }; 92 return '\x1b'; 93 case => void; 94 }; 95 return first: u32; 96}; 97 98// ROW OPERATIONS 99 100fn editor_delete(e: *editor) void = { 101 if (e.cursor_x > 0) { 102 delete(e.buf[e.cursor_y][e.cursor_x - 1]); 103 e.cursor_x -= 1; 104 } else if (e.cursor_y > 0) { 105 let previous_line = &e.buf[e.cursor_y - 1]; 106 const previous_line_len = len(previous_line); 107 append(previous_line, e.buf[e.cursor_y]...)!; 108 delete(e.buf[e.cursor_y]); 109 e.cursor_y -= 1; 110 e.cursor_x = previous_line_len; 111 }; 112 e.dirty = true; 113}; 114 115fn editor_insert(e: *editor, k: rune) void = { 116 insert(e.buf[e.cursor_y][e.cursor_x], k)!; 117 e.cursor_x += 1; 118 e.dirty = true; 119}; 120 121fn editor_newline(e: *editor) void = { 122 let current_line = &e.buf[e.cursor_y]; 123 const next_line_len = len(current_line) - e.cursor_x; 124 const next_line = if (next_line_len > 0) { 125 let next_line: []rune = alloc(['\0'...], next_line_len)!; 126 next_line[..] = current_line[e.cursor_x..]; 127 delete(e.buf[e.cursor_y][e.cursor_x..]); 128 yield next_line; 129 } else { 130 yield []: []rune; 131 }; 132 insert(e.buf[e.cursor_y + 1], next_line)!; 133 e.cursor_x = 0; 134 e.cursor_y += 1; 135 e.dirty = true; 136}; 137 138// FILE OPERATIONS 139 140fn editor_open(e: *editor, path: str) void = { 141 editor_close(e); 142 let file = os::open(path, fs::flag::RDONLY)!; 143 defer io::close(file)!; 144 let buf: [][]rune = []; 145 let first: []rune = []; 146 append(buf, first)!; 147 148 let line = 0; 149 let new_row = false; 150 for (true) { 151 match (bufio::read_rune(file)) { 152 case utf8::invalid => 153 fmt::fatal("File must be valid UTF-8"); 154 case let err: io::error => 155 fmt::fatal(io::strerror(err)); 156 case let c: rune => 157 if (new_row) { 158 new_row = false; 159 line += 1; 160 let next_line: []rune = []; 161 append(buf, next_line)!; 162 }; 163 switch (c) { 164 case '\n' => 165 new_row = true; 166 case => 167 append(buf[line], c)!; 168 }; 169 case io::EOF => 170 break; 171 }; 172 }; 173 e.buf = buf; 174 e.bufname = path; 175 e.dirty = false; 176}; 177 178fn editor_close(e: *editor) void = { 179 for (let i = 0z; i < len(e.buf); i += 1) { 180 free(e.buf[i]); 181 }; 182 free(e.buf); 183}; 184 185fn editor_save(e: *editor) void = { 186 if (e.bufname is void) { 187 e.msg = "Cannot save scratch buffer"; 188 return; 189 }; 190 let file = os::open(e.bufname as str, fs::flag::TRUNC | fs::flag::WRONLY)!; 191 defer io::close(file)!; 192 193 for (let y = 0z; y < len(e.buf); y += 1) { 194 const line = &e.buf[y]; 195 // We can't use the spread operator here :( 196 for (let x = 0z; x < len(line); x += 1) 197 fmt::fprint(file, line[x])!; 198 fmt::fprint(file, '\n')!; 199 }; 200 201 e.msg = "Saved to disk"; 202 e.dirty = false; 203}; 204 205// OUTPUT 206 207fn editor_scroll(e: *editor) void = { 208 if (e.cursor_x > len(e.buf[e.cursor_y])) 209 e.cursor_x = len(e.buf[e.cursor_y]); 210 if (e.cursor_x < e.offset_x) 211 e.offset_x = e.cursor_x; 212 if (e.cursor_x >= e.offset_x + e.screen_cols - 1) 213 e.offset_x = e.cursor_x - e.screen_cols + 1; 214 if (e.cursor_y < e.offset_y) 215 e.offset_y = e.cursor_y; 216 if (e.cursor_y >= e.offset_y + e.screen_rows - 1) 217 e.offset_y = e.cursor_y - e.screen_rows + 2; 218}; 219 220fn editor_draw_line(e: *editor, out: *memio::stream, line: *[]rune) void = { 221 if (e.offset_x > len(line)) return; 222 let length = len(line) - e.offset_x; 223 if (length > e.screen_cols) 224 length = e.screen_cols; 225 const end = e.offset_x + length; 226 let dx: size = 0; 227 for (let x = e.offset_x; x < end; x += 1) { 228 const c = line[x]; 229 switch (c) { 230 case '\t' => 231 for (let i = 0u8; i < TABSTOP; i += 1) { 232 fmt::fprint(out, ' ')!; 233 dx += 1; 234 if (dx > e.screen_cols) break; 235 }; 236 case => 237 fmt::fprint(out, c)!; 238 dx += 1; 239 }; 240 if (dx > e.screen_cols) break; 241 }; 242 for (let x = dx; x < e.screen_cols; x += 1) 243 fmt::fprint(out, key::SPACE: u32)!; 244}; 245 246fn editor_draw_screen(e: *editor) void = { 247 // Update offset to keep cursor on the screen 248 editor_scroll(e); 249 // Construct contents 250 let out = memio::dynamic(); 251 defer io::close(&out)!; 252 fmt::fprintf(&out, "{}{}", EC_CURSOR_HOME, EC_CURSOR_HIDE)!; 253 // Buffer lines 254 for (let y = 0z; y < e.screen_rows - 1; y += 1) { 255 const disp_y = e.offset_y + y; 256 if (disp_y >= len(e.buf)) { 257 fmt::fprint(&out, "~")!; 258 fmt::fprint(&out, EC_ERASE_LINE)!; 259 } else { 260 const line = e.buf[disp_y]; 261 editor_draw_line(e, &out, &line); 262 }; 263 fmt::fprint(&out, "\r\n")!; 264 }; 265 // Status line 266 // FIXME: Avoid heap allocation 267 const state = fmt::asprintf( 268 "{}:{}:{}", 269 if (e.bufname is str) e.bufname as str else SCRATCH, 270 e.cursor_x + 1, e.cursor_y + 1, 271 )!; 272 const msg = e.msg; 273 let space_count = e.screen_cols - len(state) - len(msg) - len(CONTROLS) - 1; 274 if (e.dirty) 275 space_count -= 3; 276 const spaces: []u8 = alloc([' '...], space_count)!; 277 fmt::fprintf(&out, "{}{}", 278 EC_STYLE_REVERSE, 279 CONTROLS 280 )!; 281 io::write(&out, spaces)!; 282 fmt::fprintf(&out, "{} {}{}{}", 283 msg, 284 if (e.dirty) "[*]" else "", 285 state, 286 EC_STYLE_DEFAULT 287 )!; 288 free(state); 289 free(spaces); 290 // Show and position cursor 291 let cursor_x = { 292 const line = e.buf[e.cursor_y]; 293 let tabs = 0z; 294 for (let x = e.offset_x; x < e.cursor_x; x += 1) { 295 if (line[x] == '\t') 296 tabs += 1; 297 }; 298 yield e.cursor_x - tabs + (tabs * TABSTOP: size); 299 }; 300 fmt::fprintf( 301 &out, 302 "\x1b[{};{}H", 303 e.cursor_y - e.offset_y + 1, 304 cursor_x + 1 305 )!; 306 fmt::fprint(&out, EC_CURSOR_SHOW)!; 307 // Commit to terminal 308 io::write(os::stdout_file, memio::buffer(&out))!; 309}; 310 311// INPUT 312 313fn editor_move_cursor(e: *editor, k: u32) void = { 314 switch (k) { 315 case key::ARROW_DOWN => 316 if (e.cursor_y < len(e.buf) - 1) 317 e.cursor_y += 1; 318 case key::ARROW_LEFT => 319 if (e.cursor_x > 0) { 320 e.cursor_x -= 1; 321 } else if (e.cursor_y > 0) { 322 e.cursor_y -= 1; 323 e.cursor_x = len(e.buf[e.cursor_y]); 324 }; 325 case key::ARROW_RIGHT => 326 const line = e.buf[e.cursor_y]; 327 if (e.cursor_x < len(line)) { 328 e.cursor_x += 1; 329 } else if (e.cursor_y < len(e.buf) - 1) { 330 e.cursor_y += 1; 331 e.cursor_x = 0; 332 }; 333 case key::ARROW_UP => 334 if (e.cursor_y > 0) 335 e.cursor_y -= 1; 336 case => void; 337 }; 338}; 339 340// Returns `true` if the program should exit 341fn editor_process_key(e: *editor) bool = { 342 const k = terminal_read_key(); 343 switch (k) { 344 case key::ARROW_DOWN, key::ARROW_LEFT, key::ARROW_RIGHT, key::ARROW_UP => 345 editor_move_cursor(e, k); 346 case key::HOME => 347 e.cursor_x = 0; 348 case key::END => 349 e.cursor_x = len(e.buf[e.cursor_y]); 350 case key::PAGE_DOWN => 351 e.cursor_y = e.offset_y + e.screen_rows - 2; 352 if (e.cursor_y > len(e.buf) - 1) 353 e.cursor_y = len(e.buf) - 1; 354 for (let i = 0z; i < e.screen_rows - 1; i += 1) 355 editor_move_cursor(e, key::ARROW_DOWN); 356 case key::PAGE_UP => 357 e.cursor_y = e.offset_y; 358 for (let i = 0z; i < e.screen_rows - 1; i += 1) 359 editor_move_cursor(e, key::ARROW_UP); 360 case 'q' & key::MOD_CTRL => 361 return true; 362 case 's' & key::MOD_CTRL => 363 editor_save(e); 364 case '\b', key::DELETE => 365 editor_delete(e); 366 case '\r' => 367 editor_newline(e); 368 case => 369 if (k == '\t' || ascii::isprint(k: rune)) 370 editor_insert(e, k: rune); 371 }; 372 return false; 373}; 374 375// INIT 376 377fn editor_init() editor = { 378 let termios = tty::termios_query(os::stdin_file)!; 379 fmt::fprint(os::stdout_file, EC_ALTBUF_SHOW)!; 380 tty::makeraw(&termios)!; 381 382 let buf: [][]rune = []; 383 // Invariant: There is always at least one line in a buffer 384 append(buf, [])!; 385 386 const winsize = tty::winsize(os::stdout_file)!; 387 388 return editor { 389 buf = buf, 390 bufname = void, 391 screen_cols = winsize.columns: size, 392 screen_rows = winsize.rows: size, 393 termios = termios, 394 ... 395 }; 396}; 397 398fn editor_finish(e: editor) void = { 399 editor_close(&e); 400 fmt::fprint(os::stdout_file, EC_ALTBUF_HIDE)!; 401 tty::termios_restore(&e.termios); 402 403}; 404 405export fn main() void = { 406 let e = editor_init(); 407 defer editor_finish(e); 408 409 if (len(os::args) > 1) 410 editor_open(&e, os::args[1]); 411 412 editor_draw_screen(&e); 413 for (true) { 414 e.msg = ""; 415 if (editor_process_key(&e)) break; 416 editor_draw_screen(&e); 417 }; 418};