ed

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

command.ha (13426B)


      1 use errors;
      2 use fmt;
      3 use fs;
      4 use io;
      5 use os;
      6 use os::exec;
      7 use regex;
      8 use strings;
      9 use strio;
     10 
     11 type Command = struct {
     12 	addrs: []Address,
     13 	linenums: []size,
     14 	cmdname: rune,
     15 	printmode: PrintMode,
     16 	arg: str,
     17 	arg2: str,
     18 	arg3: str,
     19 	input: []str,
     20 	subcmds: []Command,
     21 };
     22 
     23 type CommandFn = *fn(*Session, *Command) (void | Error);
     24 
     25 type PrintMode = enum {
     26 	NONE,
     27 	LIST,
     28 	NUMBER,
     29 	PRINT,
     30 };
     31 
     32 type Error = !(
     33 	InvalidAddress
     34 	| UnexpectedAddress
     35 	| InvalidDestination
     36 	| NoFilename
     37 	| BufferModified
     38 	| NoMatch
     39 	| NoPrevShCmd
     40 	| regex::error
     41 	| fs::error
     42 );
     43 
     44 type UnexpectedAddress = !void;
     45 
     46 type InvalidDestination = !void;
     47 
     48 type NoFilename = !void;
     49 
     50 type BufferModified = !void;
     51 
     52 type NoPrevShCmd = !void;
     53 
     54 fn lookupcmd(name: rune) CommandFn = {
     55 	switch (name) {
     56 	case 'a' => return &cmd_append;
     57 	case 'c' => return &cmd_change;
     58 	case 'd' => return &cmd_delete;
     59 	case 'e' => return &cmd_edit;
     60 	case 'E' => return &cmd_edit_forced;
     61 	case 'f' => return &cmd_filename;
     62 	case 'g' => return &cmd_global;
     63 	case 'G' => return &cmd_global_manual;
     64 	case 'h' => return &cmd_help;
     65 	case 'H' => return &cmd_helpmode;
     66 	case 'i' => return &cmd_insert;
     67 	case 'j' => return &cmd_join;
     68 	case 'k' => return &cmd_mark;
     69 	case 'l' => return &cmd_list;
     70 	case 'm' => return &cmd_move;
     71 	case 'n' => return &cmd_number;
     72 	case 'p' => return &cmd_print;
     73 	case 'P' => return &cmd_prompt;
     74 	case 'q' => return &cmd_quit;
     75 	case 'Q' => return &cmd_quit_forced;
     76 	case 'r' => return &cmd_read;
     77 	case 's' => return &cmd_substitute;
     78 	case 't' => return &cmd_copy;
     79 	case 'u' => return &cmd_undo;
     80 	case 'v' => return &cmd_invglobal;
     81 	case 'V' => return &cmd_invglobal_manual;
     82 	case 'w' => return &cmd_write;
     83 	case '=' => return &cmd_linenumber;
     84 	case '!' => return &cmd_shellescape;
     85 	case '\x00' => return &cmd_null;
     86 	};
     87 };
     88 
     89 fn cmd_append(s: *Session, cmd: *Command) (void | Error) = {
     90 	const n = get_linenum(cmd.linenums, s.buf.cursor);
     91 
     92 	for (let i = 0z; i < len(cmd.input); i += 1) {
     93 		const l = alloc(Line { text = cmd.input[i], ... });
     94 		debug("cmd_append(): l.text={}", l.text);
     95 		buf_insert(&s.buf, n + 1 + i, l);
     96 	};
     97 
     98 	s.buf.cursor = n + len(cmd.input);
     99 };
    100 
    101 fn cmd_change(s: *Session, cmd: *Command) (void | Error) = {
    102 	const (a, b) = get_range(
    103 		s,
    104 		&cmd.linenums,
    105 		s.buf.cursor,
    106 		s.buf.cursor,
    107 	)?;
    108 	if (a == 0) {
    109 		a = 1;
    110 		if (b == 0) {
    111 			b = 1;
    112 		};
    113 	};
    114 
    115 	buf_delete(&s.buf, a, b);
    116 
    117 	for (let i = 0z; i < len(cmd.input); i += 1) {
    118 		const l = alloc(Line { text = cmd.input[i], ... });
    119 		debug("cmd_append(): l.text={}", l.text);
    120 		buf_insert(&s.buf, a + i, l);
    121 	};
    122 
    123 	s.buf.cursor = if (len(cmd.input) == 0) {
    124 		yield if (len(s.buf.lines) == a) {
    125 			yield a - 1;
    126 		} else {
    127 			yield a;
    128 		};
    129 	} else {
    130 		yield a + len(cmd.input) - 1;
    131 	};
    132 };
    133 
    134 fn cmd_delete(s: *Session, cmd: *Command) (void | Error) = {
    135 	const (a, b) = get_range(
    136 		s,
    137 		&cmd.linenums,
    138 		s.buf.cursor,
    139 		s.buf.cursor,
    140 	)?;
    141 	assert_nonzero(s, a)?;
    142 
    143 	buf_delete(&s.buf, a, b);
    144 	s.buf.cursor = if (len(s.buf.lines) == 1) {
    145 		yield 0;
    146 	} else if (len(s.buf.lines) == a) {
    147 		yield a - 1;
    148 	} else {
    149 		yield a;
    150 	};
    151 };
    152 
    153 fn cmd_edit(s: *Session, cmd: *Command) (void | Error) = {
    154 	assert_noaddrs(s, cmd.linenums)?;
    155 
    156 	if (s.buf.modified && !s.warned) {
    157 		s.warned = true; // TODO: reset this every Command somehow
    158 		return BufferModified;
    159 	};
    160 
    161 	const fname = if (len(cmd.arg) != 0) {
    162 		s.buf.filename = cmd.arg;
    163 		yield cmd.arg;
    164 	} else if (len(s.buf.filename) != 0) {
    165 		yield s.buf.filename;
    166 	} else {
    167 		return NoFilename;
    168 	};
    169 
    170 	const h = match (os::open(fname)) {
    171 	case let err: fs::error =>
    172 		return err;
    173 	case let h: io::file =>
    174 		yield h: io::handle;
    175 	};
    176 	defer io::close(h)!;
    177 
    178 	buf_deleteall(&s.buf);
    179 	const (sz, _) = buf_read(&s.buf, h, 0);
    180 	if (!s.suppressmode) {
    181 		fmt::println(sz)!;
    182 	};
    183 	s.buf.cursor = len(s.buf.lines) - 1;
    184 };
    185 
    186 fn cmd_edit_forced(s: *Session, cmd: *Command) (void | Error) = void;
    187 
    188 fn cmd_filename(s: *Session, cmd: *Command) (void | Error) = {
    189 	assert_noaddrs(s, cmd.linenums)?;
    190 	if (len(cmd.arg) != 0) {
    191 		s.buf.filename = cmd.arg;
    192 	};
    193 	if (len(s.buf.filename) == 0) {
    194 		return NoFilename;
    195 	};
    196 	fmt::println(s.buf.filename)!;
    197 };
    198 
    199 fn cmd_global(s: *Session, cmd: *Command) (void | Error) = void;
    200 
    201 fn cmd_global_manual(s: *Session, cmd: *Command) (void | Error) = void;
    202 
    203 fn cmd_help(s: *Session, cmd: *Command) (void | Error) = {
    204 	assert_noaddrs(s, cmd.linenums)?;
    205 	if (s.lasterror != "") {
    206 		fmt::println(s.lasterror)!;
    207 	};
    208 };
    209 
    210 fn cmd_helpmode(s: *Session, cmd: *Command) (void | Error) = {
    211 	assert_noaddrs(s, cmd.linenums)?;
    212 	s.helpmode = !s.helpmode;
    213 	if (s.helpmode && s.lasterror != "") {
    214 		fmt::println(s.lasterror)!;
    215 	};
    216 };
    217 
    218 fn cmd_insert(s: *Session, cmd: *Command) (void | Error) = {
    219 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    220 	const n = if (n == 0) 1z else n;
    221 
    222 	for (let i = 0z; i < len(cmd.input); i += 1) {
    223 		const l = alloc(Line { text = cmd.input[i], ... });
    224 		debug("cmd_append(): l.text={}", l.text);
    225 		buf_insert(&s.buf, n + i, l);
    226 	};
    227 
    228 	s.buf.cursor = if (len(cmd.input) == 0) {
    229 		yield n;
    230 	} else {
    231 		yield n + len(cmd.input) - 1;
    232 	};
    233 };
    234 
    235 fn cmd_join(s: *Session, cmd: *Command) (void | Error) = {
    236 	const (a, b) = get_range(
    237 		s,
    238 		&cmd.linenums,
    239 		s.buf.cursor,
    240 		addr_nextline(&s.buf, s.buf.cursor),
    241 	)?;
    242 	assert_nonzero(s, a)?;
    243 
    244 	if (a == b) {
    245 		return;
    246 	};
    247 
    248 	let ls: []str = [];
    249 	let mark = '\x00';
    250 	for (let n = a; n <= b; n += 1) {
    251 		const l = s.buf.lines[n];
    252 		append(ls, l.text);
    253 		if (mark == '\x00') {
    254 			mark = l.mark;
    255 		};
    256 	};
    257 
    258 	const newtext = strings::concat(ls...);
    259 	const newline = alloc(Line {
    260 		text = newtext,
    261 		mark = mark,
    262 		...
    263 	});
    264 
    265 	buf_delete(&s.buf, a, b);
    266 	buf_insert(&s.buf, a, newline);
    267 };
    268 
    269 fn cmd_mark(s: *Session, cmd: *Command) (void | Error) = {
    270 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    271 	assert_nonzero(s, n)?;
    272 
    273 	// TODO: this should use ".suffix", not ".arg", and parse() should
    274 	// handle this
    275 	// TODO: check len, etc...
    276 	const mark = strings::runes(cmd.arg)[0];
    277 	debug("cmd_mark(): mark={}", mark);
    278 
    279 	:search {
    280 		debug("cmd_mark(): search A");
    281 		for (let i = 0z; i < len(s.buf.trash); i += 1) {
    282 			debug("cmd_mark(): search A i={}", i);
    283 			if (s.buf.trash[i].mark == mark) {
    284 				debug("cmd_mark(): search A i={} true", i);
    285 				s.buf.trash[i].mark = '\x00';
    286 				yield :search;
    287 			};
    288 		};
    289 		debug("cmd_mark(): search B");
    290 		for (let i = 0z; i < len(s.buf.lines); i += 1) {
    291 			debug("cmd_mark(): search B i={}", i);
    292 			if (s.buf.lines[i].mark == mark) {
    293 				debug("cmd_mark(): search B i={} true", i);
    294 				s.buf.lines[i].mark = '\x00';
    295 				yield :search;
    296 			};
    297 		};
    298 		debug("cmd_mark(): search C");
    299 	};
    300 
    301 	debug("cmd_mark(): search D");
    302 
    303 	s.buf.lines[n].mark = mark;
    304 };
    305 
    306 fn cmd_list(s: *Session, cmd: *Command) (void | Error) = {
    307 	const (a, b) = get_range(
    308 		s,
    309 		&cmd.linenums,
    310 		s.buf.cursor,
    311 		s.buf.cursor,
    312 	)?;
    313 	assert_nonzero(s, a)?;
    314 	debug("cmd_list(): (a, b)=({}, {})", a, b);
    315 
    316 	printlistlns(&s.buf, a, b)?;
    317 	s.buf.cursor = b;
    318 };
    319 
    320 fn cmd_move(s: *Session, cmd: *Command) (void | Error) = {
    321 	const (a, b) = get_range(
    322 		s,
    323 		&cmd.linenums,
    324 		s.buf.cursor,
    325 		s.buf.cursor,
    326 	)?;
    327 	assert_nonzero(s, a)?;
    328 
    329 	// TODO: parse this properly in parse.ha?
    330 	const iter = strings::iter(cmd.arg);
    331 	const n = match (scan_addr(&iter)) {
    332 	case let addr: Address =>
    333 		const n = match (exec_addr(s, addr)) {
    334 		case let n: size =>
    335 			yield n;
    336 		case InvalidAddress =>
    337 			return InvalidDestination;
    338 		};
    339 		yield n + 1; // like insert
    340 	case void =>
    341 		return InvalidAddress;
    342 	};
    343 	debug("cmd_move(): n={}", n);
    344 
    345 	if (a < n && n <= b) {
    346 		return InvalidDestination;
    347 	};
    348 
    349 	if (n == b + 1) {
    350 		return;
    351 	};
    352 
    353 	const dest = if (n > b) {
    354 		yield n - (1 + b - a);
    355 	} else {
    356 		yield n;
    357 	};
    358 
    359 	const ls = alloc(s.buf.lines[a..b+1]...); defer free(ls);
    360 	buf_delete(&s.buf, a, b);
    361 	buf_insert(&s.buf, dest, ls...);
    362 
    363 	s.buf.cursor = dest - 1 + len(ls);
    364 };
    365 
    366 fn cmd_number(s: *Session, cmd: *Command) (void | Error) = {
    367 	const (a, b) = get_range(
    368 		s,
    369 		&cmd.linenums,
    370 		s.buf.cursor,
    371 		s.buf.cursor,
    372 	)?;
    373 	assert_nonzero(s, a)?;
    374 	debug("cmd_number(): (a, b)=({}, {})", a, b);
    375 
    376 	printnumberlns(&s.buf, a, b)?;
    377 	s.buf.cursor = b;
    378 };
    379 
    380 fn cmd_print(s: *Session, cmd: *Command) (void | Error) = {
    381 	const (a, b) = get_range(
    382 		s,
    383 		&cmd.linenums,
    384 		s.buf.cursor,
    385 		s.buf.cursor,
    386 	)?;
    387 	assert_nonzero(s, a)?;
    388 
    389 	printlns(&s.buf, a, b)?;
    390 	s.buf.cursor = b;
    391 };
    392 
    393 fn cmd_prompt(s: *Session, cmd: *Command) (void | Error) = {
    394 	assert_noaddrs(s, cmd.linenums)?;
    395 	s.promptmode = !s.promptmode;
    396 };
    397 
    398 fn cmd_quit(s: *Session, cmd: *Command) (void | Error) = void;
    399 
    400 fn cmd_quit_forced(s: *Session, cmd: *Command) (void | Error) = void;
    401 
    402 fn cmd_read(s: *Session, cmd: *Command) (void | Error) = {
    403 	const n = get_linenum(cmd.linenums, s.buf.cursor);
    404 	const fname = if (len(cmd.arg) != 0) {
    405 		s.buf.filename = cmd.arg;
    406 		yield cmd.arg;
    407 	} else {
    408 		yield if (len(s.buf.filename) != 0) {
    409 			yield s.buf.filename;
    410 		} else {
    411 			return NoFilename;
    412 		};
    413 	};
    414 
    415 	const h = os::open(fname)?: io::handle;
    416 	defer io::close(h)!;
    417 
    418 	const (sz, _) = buf_read(&s.buf, h, n);
    419 	if (!s.suppressmode) {
    420 		fmt::println(sz)!;
    421 	};
    422 	s.buf.cursor = len(s.buf.lines) - 1;
    423 };
    424 
    425 fn cmd_substitute(s: *Session, cmd: *Command) (void | Error) = {
    426 	const (a, b) = get_range(
    427 		s,
    428 		&cmd.linenums,
    429 		s.buf.cursor,
    430 		s.buf.cursor,
    431 	)?;
    432 	assert_nonzero(s, a)?;
    433 
    434 	const replacement = cmd.arg2;
    435 	const re = regex::compile(cmd.arg)?; defer regex::finish(&re);
    436 
    437 	for (let i = a; i <= b; i += 1) {
    438 		const old = s.buf.lines[i].text;
    439 		const results = regex::findall(&re, old);
    440 		defer regex::result_freeall(results);
    441 
    442 		if (len(results) == 0) {
    443 			continue;
    444 		};
    445 
    446 		let iter = strings::iter(s.buf.lines[i].text);
    447 		let new = strio::dynamic(); defer io::close(&new)!;
    448 
    449 		let start = 0z;
    450 		let lbound = 0z;
    451 		let rbound = 0z;
    452 		for (let j = 0z; j < len(results); j += 1) {
    453 			lbound = results[j][0].start;
    454 			rbound = results[j][0].end;
    455 			strio::concat(&new, strings::sub(old, start, lbound))!;
    456 			strio::concat(&new, replacement)!;
    457 			start = rbound;
    458 		};
    459 		strio::concat(&new, strings::sub(old, rbound, strings::end))!;
    460 
    461 		let newline = alloc(Line {
    462 			text = strings::dup(strio::string(&new)),
    463 			...
    464 		});
    465 
    466 		buf_delete(&s.buf, i, i);
    467 		buf_insert(&s.buf, i, newline);
    468 	};
    469 };
    470 
    471 fn cmd_copy(s: *Session, cmd: *Command) (void | Error) = {
    472 	const (a, b) = get_range(
    473 		s,
    474 		&cmd.linenums,
    475 		s.buf.cursor,
    476 		s.buf.cursor,
    477 	)?;
    478 	assert_nonzero(s, a)?;
    479 
    480 	// TODO: parse this properly in parse.ha?
    481 	const iter = strings::iter(cmd.arg);
    482 	const dest = match (scan_addr(&iter)) {
    483 	case let addr: Address =>
    484 		yield 1 + (match (exec_addr(s, addr)) {
    485 		case let n: size =>
    486 			yield n;
    487 		case InvalidAddress =>
    488 			return InvalidDestination;
    489 		});
    490 	case void =>
    491 		return InvalidAddress;
    492 	};
    493 	debug("cmd_copy(): dest={}", dest);
    494 
    495 	const ls = alloc(s.buf.lines[a..b+1]...); defer free(ls);
    496 	buf_insert(&s.buf, dest, ls...);
    497 
    498 	s.buf.cursor = dest - 1 + len(ls);
    499 };
    500 
    501 fn cmd_undo(s: *Session, cmd: *Command) (void | Error) = void;
    502 
    503 fn cmd_invglobal(s: *Session, cmd: *Command) (void | Error) = void;
    504 
    505 fn cmd_invglobal_manual(s: *Session, cmd: *Command) (void | Error) = void;
    506 
    507 fn cmd_write(s: *Session, cmd: *Command) (void | Error) = {
    508 	const (a, b) = get_range(
    509 		s,
    510 		&cmd.linenums,
    511 		addr_linenum(&s.buf, 1)!,
    512 		addr_lastline(&s.buf),
    513 	)?;
    514 	assert_nonzero(s, a)?;
    515 
    516 	const fname = if (len(cmd.arg) != 0) {
    517 		s.buf.filename = cmd.arg;
    518 		yield cmd.arg;
    519 	} else {
    520 		yield if (len(s.buf.filename) != 0) {
    521 			yield s.buf.filename;
    522 		} else {
    523 			return NoFilename;
    524 		};
    525 	};
    526 
    527 	const h = match (os::open(fname)) {
    528 	case let err: fs::error =>
    529 		yield match (err) {
    530 		case errors::noentry =>
    531 			yield os::create(fname, 0o644)?: io::handle;
    532 		case =>
    533 			return err;
    534 		};
    535 	case let h: io::file =>
    536 		yield h: io::handle;
    537 	};
    538 	defer io::close(h)!;
    539 
    540 	const sz = buf_write(&s.buf, h, a, b);
    541 	if (!s.suppressmode) {
    542 		fmt::println(sz)!;
    543 	};
    544 };
    545 
    546 fn cmd_linenumber(s: *Session, cmd: *Command) (void | Error) = {
    547 	const n = get_linenum(cmd.linenums, addr_lastline(&s.buf));
    548 	fmt::println(n)!;
    549 };
    550 
    551 fn cmd_shellescape(s: *Session, cmd: *Command) (void | Error) = {
    552 	let iter = strings::iter(cmd.arg);
    553 	let new = strio::dynamic(); defer io::close(&new)!;
    554 	let preview = false;
    555 
    556 	// handling '!!'
    557 	if (strings::next(&iter) == '!') {
    558 		match (s.prev_shcmd) {
    559 		case void =>
    560 			return NoPrevShCmd;
    561 		case let s: str =>
    562 			strio::concat(&new, s)!;
    563 		};
    564 		preview = true;
    565 	} else {
    566 		strings::prev(&iter);
    567 	};
    568 
    569 	// handling '%' and '\%'
    570 	for (true) {
    571 		match (strings::next(&iter)) {
    572 		case void =>
    573 			break;
    574 		case let r: rune =>
    575 			switch (r) {
    576 			case '\\' =>
    577 				match (strings::next(&iter)) {
    578 				case void =>
    579 					break;
    580 				case let r: rune =>
    581 					strio::appendrune(&new, r)!;
    582 				};
    583 			case '%' =>
    584 				strio::concat(&new, s.buf.filename)!;
    585 				preview = true;
    586 			case =>
    587 				strio::appendrune(&new, r)!;
    588 			};
    589 		};
    590 	};
    591 
    592 	let shcmdline = strings::dup(strio::string(&new));
    593 
    594 	let shcmd = exec::cmd("sh", "-c", shcmdline)!;
    595 	let pipe = exec::pipe();
    596 	exec::addfile(&shcmd, os::stdout_file, pipe.1);
    597 	let proc = exec::start(&shcmd)!;
    598 	io::close(pipe.1)!;
    599 
    600 	let data = io::drain(pipe.0)!;
    601 	io::close(pipe.0)!;
    602 	exec::wait(&proc)!;
    603 
    604 	if (preview) {
    605 		fmt::println(shcmdline)!;
    606 	};
    607 	fmt::print(strings::fromutf8(data)!)!;
    608 	if (!s.suppressmode) {
    609 		fmt::println("!")!;
    610 	};
    611 
    612 	s.prev_shcmd = shcmdline;
    613 };
    614 
    615 fn cmd_null(s: *Session, cmd: *Command) (void | Error) = {
    616 	const n = get_linenum(
    617 		cmd.linenums,
    618 		addr_nextline(&s.buf, s.buf.cursor),
    619 	);
    620 	assert_nonzero(s, n)?;
    621 
    622 	fmt::println(s.buf.lines[n].text)!;
    623 	s.buf.cursor = n;
    624 };