ed

[hare] The standard editor
Log | Files | Refs | README | LICENSE

commit 1ed775fdc4cf5c7abaecb5cac5fa8fe36dae1b38
parent e741b8bd1d5c4129c3ece8a86789d551479a6f20
Author: Byron Torres <b@torresjrjr.com>
Date:   Fri,  2 Dec 2022 16:22:41 +0000

progress

Diffstat:
MMakefile | 2++
Aaddress.ha | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mbuffer.ha | 38++++++++++++++++++++++++++------------
Mcommand.ha | 292++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mmain.ha | 68+++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Aparse.ha | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 763 insertions(+), 38 deletions(-)

diff --git a/Makefile b/Makefile @@ -3,8 +3,10 @@ HAREFLAGS= source=\ main.ha \ + address.ha \ buffer.ha \ command.ha \ + parse.ha \ util.ha \ all: ed diff --git a/address.ha b/address.ha @@ -0,0 +1,49 @@ +use regex; + +type address = struct { + addrtype: addresstype, + lineoffset: int, +}; + +type addresstype = (size | currentline | lastline | rune); + +type currentline = void; + +type lastline = void; + +type badaddress = !void; + +fn addr_nextline(s: *session, n: size) size = { + if (len(s.buf.lines) - 1 == n) { + return n; + }; + return n + 1; +}; + +fn addr_prevline(s: *session, n: size) size = { + if (n == 0 || n == 1) { + return n; + }; + return n - 1; +}; + +fn addr_linenum(s: *session, n: size) (size | badaddress) = { + if (n > len(s.buf.lines)) { + return badaddress; + }; + return n; +}; + +fn addr_lastline(s: *session) size = { + return len(s.buf.lines) - 1z; +}; + +fn addr_mark(s: *session, mark: rune) (size | badaddress) = { + for (let n = 1z; n < len(s.buf.lines); n += 1) { + const l = s.buf.lines[n]; + if (l.mark == mark) { + return n; + }; + }; + return badaddress; +}; diff --git a/buffer.ha b/buffer.ha @@ -4,10 +4,11 @@ use io; use strings; type buffer = struct { - filename: (void | str), + filename: str, lines: []*line, trash: []*line, cursor: size, + modified: bool, }; type line = struct { @@ -19,16 +20,25 @@ type line = struct { fn buf_deleteall(buf: *buffer) void = { buf_wipetrash(buf); - for (len(buf.lines) > 1) { - insert(buf.trash[0], buf.lines[1]); - delete(buf.lines[1]); + if (len(buf.lines) > 1) { + insert(buf.trash[0], buf.lines[1..]...); + delete(buf.lines[1..]); }; }; -fn buf_read(buf: *buffer, src: io::handle, a: size) size = { +fn buf_delete(buf: *buffer, a: size, b: size) void = { + fmt::errorfln("DEBUG: buf_delete(a={}, b={})", a, b)!; buf_wipetrash(buf); - let newlines: []*line = []; + insert(buf.trash[0], buf.lines[a..b+1]...); + delete(buf.lines[a..b+1]); +}; + +fn buf_read(buf: *buffer, src: io::handle, a: size) (size, size) = { + buf_wipetrash(buf); + + let ls: []*line = []; + defer free(ls); let sz = 0z; for (true) { const bytes = match (bufio::scanline(src)) { @@ -40,25 +50,29 @@ fn buf_read(buf: *buffer, src: io::handle, a: size) size = { abort(); }; - sz += len(bytes); + sz += len(bytes) + 1; // TODO: handle newlines better const text = strings::fromutf8(bytes)!; - append(newlines, alloc(line { text = text, ... })); + append(ls, alloc(line { text = text, ... })); }; - insert(buf.lines[a + 1], newlines...); - return sz; + insert(buf.lines[a + 1], ls...); + const lenls = len(ls); + return (sz, lenls); }; -fn buf_write(buf: *buffer, dest: io::handle, a: size, b: size) void = { +fn buf_write(buf: *buffer, dest: io::handle, a: size, b: size) size = { + let sz = 0z; for (let n = a; n <= b; n += 1) { - fmt::fprintln(dest, buf.lines[n].text)!; + sz += fmt::fprintln(dest, buf.lines[n].text)!; }; + return sz; }; fn buf_wipetrash(buf: *buffer) void = { for (len(buf.trash) > 0) { free(buf.trash[0].text); + free(buf.trash[0]); delete(buf.trash[0]); }; }; diff --git a/command.ha b/command.ha @@ -1,19 +1,281 @@ +use errors; use fmt; use fs; use io; use os; -fn cmd_edit(s: *session, file: (void | str)) void = { - if (file is void) { - if (s.buffer.filename is void) { - return errormsg(s, "No current filename"); +// Executes the session's .cmd command. +fn execute(s: *session) void = { + // first, execute the addresses + let cursor = s.buf.cursor; + let lns: []size = []; + // defer free(lns); + for (let i = 0z; i < len(s.cmd.addrs); i += 1) { + const addr = s.cmd.addrs[i]; + let n = match (addr.addrtype) { + case let n: size => + yield addr_linenum(s, n)!; + case currentline => + yield cursor; + case lastline => + yield addr_lastline(s); + case let m: rune => + yield addr_mark(s, m)!; }; + n = (n: int + addr.lineoffset): size; + append(lns, n); + }; + + // then, execute the full command + switch (s.cmd.cmdtype) { + case commandtype::APPEND => + fmt::errorfln("TODO: append")!; + case commandtype::CHANGE => + fmt::errorfln("TODO: change")!; + case commandtype::DELETE => + const (a, b) = get_range(lns, cursor, cursor); + cmd_delete(s, s.cmd.arg, a, b); + case commandtype::EDIT => + cmd_edit(s, s.cmd.arg); + case commandtype::EDIT_FORCED => + fmt::errorfln("TODO: edit_forced")!; + case commandtype::FILENAME => + if (!assert_noaddrs(s, lns)) return; + cmd_filename(s, s.cmd.arg); + case commandtype::GLOBAL => + fmt::errorfln("TODO: global")!; + case commandtype::GLOBAL_INTERACTIVE => + fmt::errorfln("TODO: global_interactive")!; + case commandtype::HELP => + if (!assert_noaddrs(s, lns)) return; + cmd_help(s); + case commandtype::HELPMODE => + if (!assert_noaddrs(s, lns)) return; + cmd_helpmode(s); + case commandtype::INSERT => + fmt::errorfln("TODO: insert")!; + case commandtype::JOIN => + fmt::errorfln("TODO: join")!; + case commandtype::MARK => + fmt::errorfln("TODO: mark")!; + case commandtype::LIST => + fmt::errorfln("TODO: list")!; + case commandtype::MOVE => + fmt::errorfln("TODO: move")!; + case commandtype::NUMBER => + const (a, b) = get_range(lns, cursor, cursor); + if (a > b) { + //return badaddress; + errormsg(s, "Invalid address"); + return; + }; + cmd_number(s, a, b); + case commandtype::PRINT => + const (a, b) = get_range(lns, cursor, cursor); + cmd_print(s, a, b); + case commandtype::PROMPT=> + if (!assert_noaddrs(s, lns)) return; + cmd_prompt(s); + case commandtype::QUIT => + fmt::errorfln("TODO: quit")!; + case commandtype::READ => + const n = get_linenum(lns, cursor); + cmd_read(s, s.cmd.arg, n); + case commandtype::SUBSTITUTE => + fmt::errorfln("TODO: substitute")!; + case commandtype::COPY => + fmt::errorfln("TODO: copy")!; + case commandtype::UNDO => + fmt::errorfln("TODO: undo")!; + case commandtype::GLOBAL_INVERSE => + fmt::errorfln("TODO: global_inverse")!; + case commandtype::GLOBAL_INVERSE_INTERACTIVE => + fmt::errorfln("TODO: global_inverse_interactive")!; + case commandtype::WRITE => + const (a, b) = get_range( + lns, + addr_linenum(s, 1)!, + addr_lastline(s), + ); + cmd_write(s, s.cmd.arg, a, b); + case commandtype::LINE_NUMBER => + const n = get_linenum(lns, addr_lastline(s)); + cmd_linenumber(s, n); + case commandtype::SHELL_ESCAPE => + fmt::errorfln("TODO: shell_escape")!; + case commandtype::NULL => + const n = get_linenum(lns, if (len(lns) == 0) { + yield addr_nextline(s, cursor); + } else { + yield cursor; + }); + cmd_print(s, n, n); + + case commandtype::DEBUG_NUMBER => + dumpbuffer(&s.buf); + case => + fmt::errorfln("DEBUG: Unknown command")!; + }; +}; + +fn get_range(lns: []size, a: size, b: size) (size, size) = { + if (len(lns) == 0) { + return (a, b); + } else if (len(lns) == 1) { + return (lns[0], lns[0]); + } else { + // fmt::errorfln("DEBUG: ({}, {})", lns[len(lns)-2], lns[len(lns)-1])!; + const (a, b) = (lns[len(lns)-2], lns[len(lns)-1]); + return (a, b); + }; +}; + +fn get_linenum(lns: []size, n: size) size = { + if (len(lns) == 0) { + return n; } else { - s.buffer.filename = file; + return lns[len(lns)-1]; + }; +}; + +fn assert_noaddrs(s: *session, lns: []size) bool = { + if (len(lns) != 0) { + errormsg(s, "Unexpected address"); + return false; + }; + return true; +}; + +fn cmd_prompt(s: *session) void = { + s.promptmode = !s.promptmode; +}; + +fn cmd_linenumber(s: *session, n: size) void = { + fmt::println(n)!; +}; + +fn cmd_help(s: *session) void = { + if (s.lasterror != "") { + fmt::println(s.lasterror)!; + }; +}; + +fn cmd_helpmode(s: *session) void = { + s.helpmode = !s.helpmode; + if (s.helpmode) { + cmd_help(s); + }; +}; + +fn cmd_print(s: *session, a: size, b: size) void = { + for (let n = a; n <= b; n += 1) { + fmt::println(s.buf.lines[n].text)!; + }; + s.buf.cursor = b; +}; + +fn cmd_number(s: *session, a: size, b: size) void = { + for (let n = a; n <= b; n += 1) { + fmt::printfln("{}\t{}", n, s.buf.lines[n].text)!; + }; + s.buf.cursor = b; +}; + +fn cmd_filename(s: *session, filename: str) void = { + if (len(filename) != 0) { + s.buf.filename = filename; + }; + if (len(s.buf.filename) == 0) { + return errormsg(s, "No current filename"); + }; + fmt::println(s.buf.filename)!; +}; + +fn cmd_write(s: *session, filename: str, a: size, b: size) void = { + const fname = if (len(filename) != 0) { + s.buf.filename = filename; + yield filename; + } else { + yield if (len(s.buf.filename) != 0) { + yield s.buf.filename; + } else { + errormsg(s, "No current filename"); + return; + }; }; - const file = s.buffer.filename as str; - const h = match (os::open(file)) { + const h = match (os::open(fname)) { + case let err: fs::error => + yield match (err) { + case errors::noentry => + yield match (os::create(fname, 0o644)) { + case let h: io::file => + yield h: io::handle; + case let err: fs::error => + return errormsg(s, fs::strerror(err)); + }; + case => + return errormsg(s, fs::strerror(err)); + }; + case let h: io::file => + yield h: io::handle; + }; + defer io::close(h)!; + + const sz = buf_write(&s.buf, h, a, b); + if (!s.suppressmode) { + fmt::println(sz)!; + }; +}; + +fn cmd_read(s: *session, filename: str, a: size) void = { + const fname = if (len(filename) != 0) { + s.buf.filename = filename; + yield filename; + } else { + yield if (len(s.buf.filename) != 0) { + yield s.buf.filename; + } else { + errormsg(s, "No current filename"); + return; + }; + }; + + const h = match (os::open(fname)) { + case let err: fs::error => + return errormsg(s, fs::strerror(err)); + case let h: io::file => + yield h: io::handle; + }; + defer io::close(h)!; + + const (sz, _) = buf_read(&s.buf, h, a); + if (!s.suppressmode) { + fmt::println(sz)!; + }; + s.buf.cursor = len(s.buf.lines) - 1; +}; + +fn cmd_edit(s: *session, filename: str) void = { + if (s.buf.modified && !s.warned) { + errormsg(s, "Warning: buffer modified"); + s.warned = true; + return; + }; + + const fname = if (len(filename) != 0) { + s.buf.filename = filename; + yield filename; + } else { + yield if (len(s.buf.filename) != 0) { + yield s.buf.filename; + } else { + errormsg(s, "No current filename"); + return; + }; + }; + + const h = match (os::open(fname)) { case let err: fs::error => return errormsg(s, fs::strerror(err)); case let h: io::file => @@ -21,9 +283,21 @@ fn cmd_edit(s: *session, file: (void | str)) void = { }; defer io::close(h)!; - buf_deleteall(&s.buffer); - const sz = buf_read(&s.buffer, h, 0); + buf_deleteall(&s.buf); + const (sz, _) = buf_read(&s.buf, h, 0); if (!s.suppressmode) { fmt::println(sz)!; }; + s.buf.cursor = len(s.buf.lines) - 1; +}; + +fn cmd_delete(s: *session, filename: str, a: size, b: size) void = { + buf_delete(&s.buf, a, b); + s.buf.cursor = if (len(s.buf.lines) == 1) { + yield 0; + } else if (len(s.buf.lines) == a) { + yield a - 1; + } else { + yield a; + }; }; diff --git a/main.ha b/main.ha @@ -1,15 +1,26 @@ use bufio; +use encoding::utf8; use fmt; use getopt; use io; use os; +use strings; type session = struct { - buffer: buffer, + buf: buffer, + mode: mode, + cmd: command, helpmode: bool, + lasterror: str, suppressmode: bool, promptmode: bool, prompt: str, + warned: bool, +}; + +type mode = enum { + COMMAND, + INPUT, }; export fn main() void = { @@ -22,13 +33,14 @@ export fn main() void = { const cmd = getopt::parse(os::args, help...); defer getopt::finish(&cmd); - const s = session{ - buffer = buffer{ - filename = void, - lines = [ alloc(line{ ... }) ], + const s = session { + buf = buffer { + lines = [ alloc(line { ... }) ], trash = [], ... }, + cmd = command { ... }, + prompt = "*", ... }; @@ -54,32 +66,53 @@ export fn main() void = { case "" => fmt::fatal("Invalid filename ''"); case => - s.buffer.filename = cmd.args[0]; + s.buf.filename = cmd.args[0]; }; }; - if (s.buffer.filename is str) { - cmd_edit(&s, s.buffer.filename as str); + if (len(s.buf.filename) != 0) { + cmd_edit(&s, s.buf.filename); }; for (true) :repl { - fmt::error(s.prompt)!; + if (s.promptmode) { + fmt::error(s.prompt)!; + }; const rawline = match (bufio::scanline(os::stdin)) { - case let rawline: []u8 => - yield rawline; + case let bs: []u8 => + yield bs; case io::EOF => fmt::println()!; break; - case => - abort(); + case let err: io::error => + fmt::fatal(io::strerror(err)); }; defer free(rawline); - const input = fmt::bsprint(rawline); + const input = match (strings::fromutf8(rawline)) { + case let s: str => + yield s; + case encoding::utf8::invalid => + fmt::errorln("Error: Invalid UTF-8 input")!; + continue; + }; + + switch (s.mode) { + case mode::COMMAND => + if (parse(&s.cmd, input)) { + execute(&s); + }; + case mode::INPUT => + if (input == ".") { + s.mode = mode::COMMAND; + continue; + }; + append(s.cmd.input, input); + }; }; - dumpbuffer(&s.buffer); + // dumpbuffer(&s.buf); }; @noreturn fn exit_usage(help: []getopt::help) void = { @@ -87,9 +120,10 @@ export fn main() void = { os::exit(1); }; -fn errormsg(s: *session, msgfmt: str, args: fmt::field...) void = { +fn errormsg(s: *session, msg: str) void = { fmt::errorln('?')!; + s.lasterror = msg; if (s.helpmode) { - fmt::errorfln(msgfmt)!; + fmt::errorfln(msg)!; }; }; diff --git a/parse.ha b/parse.ha @@ -0,0 +1,352 @@ +use fmt; + +use ascii; +use strconv; +use strings; + +type command = struct { + addrs: []address, + cmdtype: commandtype, + suffix: suffix, + arg: str, + input: []str, + subcmds: []command, +}; + +type commandtype = enum { + APPEND, + CHANGE, + DELETE, + EDIT, + EDIT_FORCED, + FILENAME, + GLOBAL, + GLOBAL_INTERACTIVE, + HELP, + HELPMODE, + INSERT, + JOIN, + MARK, + LIST, + MOVE, + NUMBER, + PRINT, + PROMPT, + QUIT, + QUIT_FORCED, + READ, + SUBSTITUTE, + COPY, + UNDO, + GLOBAL_INVERSE, + GLOBAL_INVERSE_INTERACTIVE, + WRITE, + LINE_NUMBER, + SHELL_ESCAPE, + NULL, + + DEBUG_NUMBER, +}; + +type suffix = enum { + NONE, + LIST, + NUMBER, + PRINT, +}; + +// Parses inputted commands. Returns true on terminal input. +fn parse(cmd: *command, input: str) bool = { + if (len(input) != 1) { + void; + }; + + const iter = strings::iter(input); + + cmd.addrs = scan_addrs(&iter); + cmd.cmdtype = scan_cmdtype(&iter); + cmd.arg = scan_arg(&iter); + + return true; +}; + +fn scan_addrs(iter: *strings::iterator) []address = { + let addrs: []address = []; + let specialfirst = false; + + scan_blanks(iter); + match (strings::next(iter)) { + case void => + return addrs; + case let r: rune => + switch (r) { + case ',' => + append(addrs, address { + addrtype = 1z, + lineoffset = 0, + }); + specialfirst = true; + case ';' => + append(addrs, address { + addrtype = currentline, + lineoffset = 0, + }); + specialfirst = true; + case => + strings::prev(iter); + }; + }; + + for (true) { + match (scan_addr(iter)) { + case void => + if (specialfirst) { + append(addrs, address { + addrtype = lastline, + lineoffset = 0, + }); + } else if (len(addrs) > 0) { + const prevaddr = addrs[len(addrs)-1]; + append(addrs, prevaddr); + } else { + break; + }; + case let addr: address => + append(addrs, addr); + }; + + specialfirst = false; + + scan_blanks(iter); + match (strings::next(iter)) { + case void => + break; + case let r: rune => + switch (r) { + case ',', ';' => + void; + case => + strings::prev(iter); + break; + }; + }; + }; + + // fmt::errorfln("DEBUG: scan_addrs() len(addrs)={}", len(addrs))!; + + return addrs; +}; + +fn scan_addr(iter: *strings::iterator) (address | void) = { + scan_blanks(iter); + let r = match (strings::next(iter)) { + case void => + return void; + case let r: rune => + yield r; + }; + + // fmt::errorfln("DEBUG: scan_addr() r={}", r)!; + + const addrtype: (addresstype | void) = if (r == '.') { + yield currentline; + } else if (r == '$') { + yield lastline; + } else if (ascii::isdigit(r)) { + strings::prev(iter); + yield scan_uint(iter): size; + } else if (r == '\'') { + yield scan_mark(iter); + } else if (r == '/') { + // const re = scan_regex(iter, r);j + // ... + abort("TODO: /regex/"); + } else if (r == '?') { + // const re = scan_regex(iter, r);j + // ... + abort("TODO: ?regex?"); + } else { + strings::prev(iter); + yield void; + }; + + const offs = scan_offsets(iter); + + const addrtype: addresstype = match (addrtype) { + case void => + yield if (len(offs) == 0) { + return void; + } else { + yield currentline; + }; + case => + yield addrtype as addresstype; + }; + + let addr = address { + addrtype = addrtype, + lineoffset = 0, + }; + + for (let i = 0z; i < len(offs); i += 1) { + addr.lineoffset += offs[i]; + }; + + return addr; +}; + +fn scan_offsets(iter: *strings::iterator) []int = { + let offs: []int = []; + + for (true) { + scan_blanks(iter); + + match (strings::next(iter)) { + case void => + return offs; + case let r: rune => + if (r == '+') { + append(offs, scan_offset(iter)); + } else if (r == '-') { + append(offs, scan_offset(iter)); + } else if (ascii::isdigit(r)) { + strings::prev(iter); + append(offs, scan_uint(iter): int); + } else { + strings::prev(iter); + break; + }; + }; + }; + + return offs; +}; + +fn scan_offset(iter: *strings::iterator) int = { + match (strings::next(iter)) { + case void => + return 1; + case let r: rune => + strings::prev(iter); + if (ascii::isdigit(r)) { + return scan_uint(iter): int; + } else { + return 1; + }; + }; +}; + +fn scan_cmdtype(iter: *strings::iterator) commandtype = { + let r = match (strings::next(iter)) { + case void => + return commandtype::NULL; + case let r: rune => + yield r; + }; + + switch (r) { + case '=' => return commandtype::LINE_NUMBER; + case 'E' => return commandtype::EDIT_FORCED; + case 'G' => return commandtype::GLOBAL_INTERACTIVE; + case 'H' => return commandtype::HELPMODE; + case 'P' => return commandtype::PROMPT; + case 'Q' => return commandtype::QUIT_FORCED; + case 'V' => return commandtype::GLOBAL_INVERSE_INTERACTIVE; + case 'a' => return commandtype::APPEND; + case 'c' => return commandtype::CHANGE; + case 'd' => return commandtype::DELETE; + case 'e' => return commandtype::EDIT; + case 'f' => return commandtype::FILENAME; + case 'g' => return commandtype::GLOBAL; + case 'h' => return commandtype::HELP; + case 'i' => return commandtype::INSERT; + case 'j' => return commandtype::JOIN; + case 'k' => return commandtype::MARK; + case 'l' => return commandtype::LIST; + case 'm' => return commandtype::MOVE; + case 'n' => return commandtype::NUMBER; + case 'p' => return commandtype::PRINT; + case 'q' => return commandtype::QUIT; + case 'r' => return commandtype::READ; + case 's' => return commandtype::SUBSTITUTE; + case 't' => return commandtype::COPY; + case 'u' => return commandtype::UNDO; + case 'v' => return commandtype::GLOBAL_INVERSE; + case 'w' => return commandtype::WRITE; + + case 'b' => return commandtype::DEBUG_NUMBER; + case => abort("Invalid command"); + }; +}; + + +fn scan_arg(iter: *strings::iterator) str = { + let rs: []rune = []; + for (true) { + match (strings::next(iter)) { + case void => + break; + case let r: rune => + append(rs, r); + }; + }; + return strings::trim(strings::fromrunes(rs)); +}; + +fn scan_mark(iter: *strings::iterator) rune = { + match (strings::next(iter)) { + case void => + abort(); + case let r: rune => + if (ascii::isalpha(r)) { // TODO: cover all mark chars + return r; + } else { + abort(); + }; + }; +}; + +fn scan_uint(iter: *strings::iterator) uint = { + let num: []u8 = []; + defer free(num); + for (true) { + let r = match (strings::next(iter)) { + case void => + break; + case let r: rune => + yield r; + }; + + if (ascii::isdigit(r)) { + append(num, r: u32: u8); + } else { + strings::prev(iter); + break; + }; + }; + + if (len(num) == 0) { + return 0; + }; + + match (strconv::stou(strings::fromutf8(num)!)) { + case (strconv::invalid | strconv::overflow) => + abort("Invalid"); + case let u: uint => + return u; + }; +}; + +fn scan_blanks(iter: *strings::iterator) void = { + for (true) { + match (strings::next(iter)) { + case void => + return; + case let r: rune => + if (!ascii::isblank(r)) { + strings::prev(iter); + return; + }; + }; + }; +};