command.ha (15660B)
1 use errors; 2 use fmt; 3 use fs; 4 use io; 5 use memio; 6 use os; 7 use os::exec; 8 use regex; 9 use strings; 10 11 type Command = struct{ 12 addrs: []Address, 13 linenums: []size, 14 name: rune, 15 suffix: rune, 16 printmode: PrintMode, 17 delim: rune, 18 arg1: str, 19 arg2: str, 20 arg3: str, 21 count: size, 22 flag_global: bool, 23 textinput: []str, 24 subcmds: []Command, 25 }; 26 27 type CommandFn = *fn(*Session, *Command) (void | Error); 28 29 type PrintMode = enum{ 30 NONE, 31 PLAIN, 32 NUMBER, 33 LIST, 34 }; 35 36 type CmdError = !( 37 InvalidAddress 38 | UnexpectedAddress 39 | InvalidDestination 40 | NoFilename 41 | WarnBufferModified 42 | NoMatch 43 | NoPrevRegex 44 | NoPrevShCmd 45 | NoPrevGlobalSubCmd 46 | InvalidGlobalSubCmd 47 | NoHistory 48 | regex::error 49 | fs::error 50 | os::exec::error 51 ); 52 53 type UnexpectedAddress = !void; 54 55 type InvalidDestination = !void; 56 57 type NoFilename = !void; 58 59 type WarnBufferModified = !void; 60 61 type NoPrevShCmd = !void; 62 63 type NoPrevGlobalSubCmd = !void; 64 65 type InvalidGlobalSubCmd = !rune; 66 67 fn command_finish(cmd: *Command) void = { 68 //debug("command_finish(): delete(cmd.linenums[..])"); 69 delete(cmd.linenums[..]); 70 //debug("command_finish(): free(cmd.arg1)"); 71 free(cmd.arg1); 72 //debug("command_finish(): free(cmd.arg2)"); 73 free(cmd.arg2); 74 //debug("command_finish(): free(cmd.arg3)"); 75 free(cmd.arg3); 76 //debug("command_finish(): delete(cmd.input[..])"); 77 delete(cmd.textinput[..]); 78 //debug("command_finish(): END"); 79 // TODO: free other fields? 80 // TODO: make a separate "fn command_clear()" ? probably not 81 }; 82 83 fn lookupcmd(name: rune) CommandFn = { 84 switch (name) { 85 case 'a' => return &cmd_append; 86 case 'c' => return &cmd_change; 87 case 'd' => return &cmd_delete; 88 case 'e' => return &cmd_edit; 89 case 'E' => return &cmd_edit_forced; 90 case 'f' => return &cmd_filename; 91 case 'g' => return &cmd_global; 92 case 'G' => return &cmd_global_ia; 93 case 'h' => return &cmd_help; 94 case 'H' => return &cmd_helpmode; 95 case 'i' => return &cmd_insert; 96 case 'j' => return &cmd_join; 97 case 'k' => return &cmd_mark; 98 case 'l' => return &cmd_list; 99 case 'm' => return &cmd_move; 100 case 'n' => return &cmd_number; 101 case 'p' => return &cmd_print; 102 case 'P' => return &cmd_prompt; 103 case 'q' => return &cmd_quit; 104 case 'Q' => return &cmd_quit_forced; 105 case 'r' => return &cmd_read; 106 case 's' => return &cmd_substitute; 107 case 't' => return &cmd_copy; 108 case 'u' => return &cmd_undo; 109 case 'v' => return &cmd_vglobal; 110 case 'V' => return &cmd_vglobal_ia; 111 case 'w' => return &cmd_write; 112 case '=' => return &cmd_linenumber; 113 case '!' => return &cmd_shellescape; 114 case NUL => return &cmd_null; 115 case => abort(); 116 }; 117 }; 118 119 fn cmd_append(s: *Session, cmd: *Command) (void | Error) = { 120 s.warned = false; 121 122 const n = get_linenum(cmd.linenums, s.buf.cursor); 123 124 if (len(cmd.textinput) > 0) 125 hist_newseq(s.buf); 126 127 for (let i = 0z; i < len(cmd.textinput); i += 1) { 128 const line = alloc(Line{ text = cmd.textinput[i], ... }); 129 buf_insert(s.buf, n + 1 + i, line); 130 }; 131 132 if (len(cmd.textinput) > 0) 133 hist_tidy(s, s.buf, 0); 134 135 s.buf.cursor = n + len(cmd.textinput); 136 printmode(s, cmd)?; 137 }; 138 139 fn cmd_change(s: *Session, cmd: *Command) (void | Error) = { 140 s.warned = false; 141 142 const (a, b) = get_range( 143 s, 144 &cmd.linenums, 145 s.buf.cursor, 146 s.buf.cursor, 147 )?; 148 if (a == 0) { 149 a = 1; 150 if (b == 0) { 151 b = 1; 152 }; 153 }; 154 155 hist_newseq(s.buf); 156 buf_delete(s.buf, a, b); 157 hist_tidy(s, s.buf, b - a + 1); 158 159 160 for (let i = 0z; i < len(cmd.textinput); i += 1) { 161 const line = alloc(Line{ text = cmd.textinput[i], ... }); 162 buf_insert(s.buf, a + i, line); 163 }; 164 165 s.buf.cursor = 166 if (len(cmd.textinput) == 0) 167 if (len(s.buf.lines) == a) 168 a - 1 169 else 170 a 171 else 172 a + len(cmd.textinput) - 1; 173 printmode(s, cmd)?; 174 }; 175 176 fn cmd_delete(s: *Session, cmd: *Command) (void | Error) = { 177 s.warned = false; 178 179 const (a, b) = get_range( 180 s, 181 &cmd.linenums, 182 s.buf.cursor, 183 s.buf.cursor, 184 )?; 185 assert_nonzero(s, a)?; 186 187 hist_newseq(s.buf); 188 buf_delete(s.buf, a, b); 189 hist_tidy(s, s.buf, b - a + 1); 190 191 s.buf.cursor = 192 if (len(s.buf.lines) == 1) 193 0 194 else if (len(s.buf.lines) == a) 195 a - 1 196 else 197 a; 198 printmode(s, cmd)?; 199 }; 200 201 fn cmd_edit(s: *Session, cmd: *Command) (void | Error) = { 202 edit(s, cmd, false)?; 203 }; 204 205 fn cmd_edit_forced(s: *Session, cmd: *Command) (void | Error) = { 206 edit(s, cmd, true)?; 207 }; 208 209 fn cmd_filename(s: *Session, cmd: *Command) (void | Error) = { 210 s.warned = false; 211 212 assert_noaddrs(s, cmd.linenums)?; 213 214 fmt::println(filename(s, cmd, true)?)!; 215 }; 216 217 fn cmd_global(s: *Session, cmd: *Command) (void | Error) = { 218 global(s, cmd, true)?; 219 }; 220 221 fn cmd_global_ia(s: *Session, cmd: *Command) (void | Error) = { 222 global_ia(s, cmd, true)?; 223 }; 224 225 fn cmd_help(s: *Session, cmd: *Command) (void | Error) = { 226 s.warned = false; 227 228 assert_noaddrs(s, cmd.linenums)?; 229 230 if (s.lasterror is Error) 231 fmt::println(strerror(s.lasterror as Error))!; 232 233 printmode(s, cmd)?; 234 }; 235 236 fn cmd_helpmode(s: *Session, cmd: *Command) (void | Error) = { 237 s.warned = false; 238 239 assert_noaddrs(s, cmd.linenums)?; 240 241 s.helpmode = !s.helpmode; 242 243 if (s.helpmode && s.lasterror is Error) 244 fmt::println(strerror(s.lasterror as Error))!; 245 246 printmode(s, cmd)?; 247 }; 248 249 fn cmd_insert(s: *Session, cmd: *Command) (void | Error) = { 250 s.warned = false; 251 252 const n = get_linenum(cmd.linenums, s.buf.cursor); 253 const n = if (n == 0) 1z else n; 254 255 if (len(cmd.textinput) > 0) 256 hist_newseq(s.buf); 257 258 for (let i = 0z; i < len(cmd.textinput); i += 1) { 259 const line = alloc(Line{ text = cmd.textinput[i], ... }); 260 buf_insert(s.buf, n + i, line); 261 }; 262 263 if (len(cmd.textinput) > 0) 264 hist_tidy(s, s.buf, 0); 265 266 s.buf.cursor = 267 if (len(cmd.textinput) == 0) 268 n 269 else 270 n + len(cmd.textinput) - 1; 271 printmode(s, cmd)?; 272 }; 273 274 fn cmd_join(s: *Session, cmd: *Command) (void | Error) = { 275 s.warned = false; 276 277 const (a, b) = get_range( 278 s, 279 &cmd.linenums, 280 s.buf.cursor, 281 addr_nextline(s.buf, s.buf.cursor), 282 )?; 283 assert_nonzero(s, a)?; 284 if (a == b) 285 return; 286 287 let ls: []str = []; 288 let mark = NUL; 289 for (let n = a; n <= b; n += 1) { 290 const line = s.buf.lines[n]; 291 append(ls, line.text); 292 if (mark == NUL) 293 mark = line.mark; 294 }; 295 296 const newtext = strings::concat(ls...); 297 const newline = alloc(Line{ 298 text = newtext, 299 mark = mark, 300 ... 301 }); 302 303 hist_newseq(s.buf); 304 buf_delete(s.buf, a, b); 305 buf_insert(s.buf, a, newline); 306 hist_tidy(s, s.buf, b - a + 1); 307 308 s.buf.cursor = a; 309 printmode(s, cmd)?; 310 }; 311 312 fn cmd_mark(s: *Session, cmd: *Command) (void | Error) = { 313 s.warned = false; 314 315 const n = get_linenum(cmd.linenums, s.buf.cursor); 316 assert_nonzero(s, n)?; 317 318 const mark = cmd.suffix; 319 320 :clearmark { 321 for (let i = 0z; i < len(s.buf.lines); i += 1) { 322 if (s.buf.lines[i].mark == mark) { 323 s.buf.lines[i].mark = NUL; 324 yield :clearmark; 325 }; 326 }; 327 for (let i = 0z; i < len(s.buf.trash); i += 1) { 328 if (s.buf.trash[i].mark == mark) { 329 s.buf.trash[i].mark = NUL; 330 yield :clearmark; 331 }; 332 }; 333 }; 334 335 s.buf.lines[n].mark = mark; 336 337 printmode(s, cmd)?; 338 }; 339 340 fn cmd_list(s: *Session, cmd: *Command) (void | Error) = { 341 s.warned = false; 342 343 const (a, b) = get_range( 344 s, 345 &cmd.linenums, 346 s.buf.cursor, 347 s.buf.cursor, 348 )?; 349 assert_nonzero(s, a)?; 350 351 printlist(s.buf, a, b)?; 352 353 s.buf.cursor = b; 354 }; 355 356 fn cmd_move(s: *Session, cmd: *Command) (void | Error) = { 357 s.warned = false; 358 359 const (a, b) = get_range( 360 s, 361 &cmd.linenums, 362 s.buf.cursor, 363 s.buf.cursor, 364 )?; 365 assert_nonzero(s, a)?; 366 367 // TODO: parse this properly in parse.ha? 368 const t = strings::iter(cmd.arg1); 369 const addr = match (scan_addr(&t)) { 370 case let a: Address => 371 yield a; 372 case void => 373 return InvalidAddress; 374 }; 375 const n = match (exec_addr(s, addr)) { 376 case let n: size => 377 yield n + 1; // like insert 378 case InvalidAddress => 379 return InvalidDestination; 380 }; 381 382 if (a < n && n <= b) 383 return InvalidDestination; 384 385 if (n == b + 1) 386 return; // no-op 387 388 const dest = 389 if (n > b) 390 n - (1 + b - a) 391 else 392 n; 393 394 const lines = alloc(s.buf.lines[a..b+1]...); defer free(lines); // TODO: mem? 395 396 hist_newseq(s.buf); 397 buf_delete(s.buf, a, b); 398 buf_insert(s.buf, dest, lines...); 399 hist_tidy(s, s.buf, b - a + 1); 400 401 s.buf.cursor = dest - 1 + len(lines); 402 printmode(s, cmd)?; 403 }; 404 405 fn cmd_number(s: *Session, cmd: *Command) (void | Error) = { 406 s.warned = false; 407 408 const (a, b) = get_range( 409 s, 410 &cmd.linenums, 411 s.buf.cursor, 412 s.buf.cursor, 413 )?; 414 assert_nonzero(s, a)?; 415 416 printnumber(s.buf, a, b)?; 417 418 s.buf.cursor = b; 419 }; 420 421 fn cmd_print(s: *Session, cmd: *Command) (void | Error) = { 422 s.warned = false; 423 424 const (a, b) = get_range( 425 s, 426 &cmd.linenums, 427 s.buf.cursor, 428 s.buf.cursor, 429 )?; 430 assert_nonzero(s, a)?; 431 432 printplain(s.buf, a, b)?; 433 434 s.buf.cursor = b; 435 }; 436 437 fn cmd_prompt(s: *Session, cmd: *Command) (void | Error) = { 438 s.warned = false; 439 440 assert_noaddrs(s, cmd.linenums)?; 441 442 s.promptmode = !s.promptmode; 443 444 printmode(s, cmd)?; 445 }; 446 447 fn cmd_quit(s: *Session, cmd: *Command) (void | Error) = { 448 assert_noaddrs(s, cmd.linenums)?; 449 450 if (s.buf.modified && !s.warned) { 451 s.warned = true; 452 return WarnBufferModified; 453 }; 454 455 return Quit; 456 }; 457 458 fn cmd_quit_forced(s: *Session, cmd: *Command) (void | Error) = { 459 assert_noaddrs(s, cmd.linenums)?; 460 461 return Quit; 462 }; 463 464 fn cmd_read(s: *Session, cmd: *Command) (void | Error) = { 465 s.warned = false; 466 467 const n = get_linenum(cmd.linenums, addr_lastline(s.buf)); 468 469 const rd: io::handle = 470 if (!strings::hasprefix(cmd.arg1, "!")) { 471 yield os::open(filename(s, cmd, false)?)?; 472 } else { 473 let shcmdline = strings::cut(cmd.arg1, "!").1; 474 let shcmd = exec::cmd("sh", "-c", shcmdline)?; 475 let pipe = exec::pipe(); 476 exec::addfile(&shcmd, os::stdout_file, pipe.1); 477 let proc = exec::start(&shcmd)?; 478 io::close(pipe.1)!; 479 exec::wait(&proc)?; 480 yield pipe.0; 481 }; 482 defer io::close(rd)!; 483 484 hist_newseq(s.buf); 485 486 const (sz, linecnt) = buf_read(s.buf, rd, n)?; 487 488 if (linecnt == 0) 489 hist_discardseq(s.buf) 490 else 491 hist_tidy(s, s.buf, 0); 492 493 if (!s.suppressmode) 494 fmt::println(sz)!; 495 496 s.buf.cursor = len(s.buf.lines) - 1; 497 }; 498 499 fn cmd_substitute(s: *Session, cmd: *Command) (void | Error) = { 500 s.warned = false; 501 502 const (a, b) = get_range( 503 s, 504 &cmd.linenums, 505 s.buf.cursor, 506 s.buf.cursor, 507 )?; 508 assert_nonzero(s, a)?; 509 510 // TODO: implement '&', '%' 511 512 const re = newregex(s, cmd.arg1)?; 513 defer regex::finish(&re); 514 const replacement = strings::join("\n", cmd.textinput...); 515 defer free(replacement); 516 const count = if (cmd.count == 0z) 0z else cmd.count - 1z; 517 518 let changes = 0z; 519 520 hist_newseq(s.buf); 521 522 for (let n = a; n <= b; n += 1) { 523 const old = s.buf.lines[n].text; 524 const results = regex::findall(&re, old); 525 defer regex::result_freeall(results); 526 527 if (len(results) == 0) 528 continue; 529 530 let new = memio::dynamic(); defer io::close(&new)!; 531 532 let start = 0z; 533 let lbound = 0z; 534 let rbound = 0z; 535 for (let i = count; i < len(results); i += 1) { 536 lbound = results[i][0].start; 537 rbound = results[i][0].end; 538 memio::concat(&new, strings::sub(old, start, lbound))!; 539 memio::concat(&new, replacement)!; 540 start = rbound; 541 542 if (!cmd.flag_global) 543 break; 544 }; 545 memio::concat(&new, strings::sub(old, rbound, strings::end))!; 546 let newtext = memio::string(&new)!; 547 let newtexts = strings::split(newtext, "\n"); 548 549 buf_delete(s.buf, n, n); 550 551 for (let i = 0z; i < len(newtexts); i += 1) { 552 let newtext = newtexts[i]; 553 let newline = alloc(Line{ 554 text = strings::dup(newtext), 555 ... 556 }); 557 558 buf_insert(s.buf, n + i, newline); 559 560 s.buf.cursor = n + i; 561 }; 562 563 changes += 1; 564 }; 565 566 if (changes == 0) { 567 hist_discardseq(s.buf); 568 return NoMatch; 569 } else { 570 hist_tidy(s, s.buf, changes); 571 }; 572 573 printmode(s, cmd)?; 574 }; 575 576 fn cmd_copy(s: *Session, cmd: *Command) (void | Error) = { 577 s.warned = false; 578 579 const (a, b) = get_range( 580 s, 581 &cmd.linenums, 582 s.buf.cursor, 583 s.buf.cursor, 584 )?; 585 assert_nonzero(s, a)?; 586 587 // TODO: parse this properly in parse.ha? 588 const t = strings::iter(cmd.arg1); 589 const addr = match (scan_addr(&t)) { 590 case let a: Address => 591 yield a; 592 case void => 593 return InvalidAddress; 594 }; 595 const dest = match (exec_addr(s, addr)) { 596 case let n: size => 597 yield n + 1; 598 case InvalidAddress => 599 return InvalidDestination; 600 }; 601 602 const lines = alloc(s.buf.lines[a..b+1]...); defer free(lines); // TODO: ? 603 604 hist_newseq(s.buf); 605 buf_insert(s.buf, dest, lines...); 606 hist_tidy(s, s.buf, 0); 607 608 s.buf.cursor = dest - 1 + len(lines); 609 printmode(s, cmd)?; 610 }; 611 612 fn cmd_undo(s: *Session, cmd: *Command) (void | Error) = { 613 s.warned = false; 614 615 assert_noaddrs(s, cmd.linenums)?; 616 617 switch (s.undomode) { 618 case UndoMode::POSIX => 619 if (s.buf.redolastchange) { 620 hist_redo(s.buf)?; 621 s.buf.redolastchange = false; 622 } else { 623 hist_undo(s.buf)?; 624 s.buf.redolastchange = true; 625 }; 626 case UndoMode::FULL => 627 hist_undo(s.buf)?; 628 hist_discardseq(s.buf); 629 }; 630 }; 631 632 fn cmd_vglobal(s: *Session, cmd: *Command) (void | Error) = { 633 global(s, cmd, false)?; 634 }; 635 636 fn cmd_vglobal_ia(s: *Session, cmd: *Command) (void | Error) = { 637 global_ia(s, cmd, false)?; 638 }; 639 640 fn cmd_write(s: *Session, cmd: *Command) (void | Error) = { 641 s.warned = false; 642 643 const (a, b) = get_range( 644 s, 645 &cmd.linenums, 646 addr_linenum(s.buf, 1)?, 647 addr_lastline(s.buf), 648 )?; 649 assert_nonzero(s, a)?; 650 651 let proc: (void | exec::process) = void; 652 const wr: io::handle = 653 if (!strings::hasprefix(cmd.arg1, "!")) { 654 yield os::create(filename(s, cmd, false)?, 0o644)?; 655 } else { 656 let shcmdline = strings::cut(cmd.arg1, "!").1; 657 let shcmd = exec::cmd("sh", "-c", shcmdline)?; 658 let pipe = exec::pipe(); 659 exec::addfile(&shcmd, os::stdin_file, pipe.0); 660 proc = exec::start(&shcmd)?; 661 yield pipe.1; 662 }; 663 664 const sz = buf_write(s.buf, wr, a, b)?; 665 io::close(wr)!; 666 667 // XXX: create a stdout pipe and copy it out here instead? 668 if (proc is exec::process) 669 exec::wait(&(proc as exec::process))?; 670 671 if (!s.suppressmode) 672 fmt::println(sz)!; 673 674 if (proc is void && a == 1 && b == len(s.buf.lines) - 1) 675 // the entire buffer has been written to a file. 676 s.buf.modified = false; 677 678 if (cmd.suffix == 'q') { 679 if (s.buf.modified && !s.warned) { 680 s.warned = true; 681 return WarnBufferModified; 682 }; 683 return Quit; 684 }; 685 }; 686 687 fn cmd_linenumber(s: *Session, cmd: *Command) (void | Error) = { 688 s.warned = false; 689 690 const n = get_linenum(cmd.linenums, addr_lastline(s.buf)); 691 fmt::println(n)!; 692 693 printmode(s, cmd)?; 694 }; 695 696 fn cmd_shellescape(s: *Session, cmd: *Command) (void | Error) = { 697 s.warned = false; 698 699 assert_noaddrs(s, cmd.linenums)?; 700 701 let t = strings::iter(cmd.arg1); 702 let new = memio::dynamic(); defer io::close(&new)!; 703 let preview = false; 704 705 // handling '!!' 706 if (strings::next(&t) == '!') { 707 match (s.lastshcmd) { 708 case void => 709 return NoPrevShCmd; 710 case let s: str => 711 memio::concat(&new, s)!; 712 }; 713 preview = true; 714 } else { 715 strings::prev(&t); 716 }; 717 718 // handling '%' and '\%' 719 for (let r: rune => strings::next(&t)) { 720 switch (r) { 721 case => 722 memio::appendrune(&new, r)!; 723 case '%' => 724 if (strings::prev(&t) == '\\') { 725 strings::next(&t); 726 memio::appendrune(&new, '%')!; 727 continue; 728 } else { 729 strings::next(&t); 730 }; 731 732 if (s.buf.filename == "") 733 return NoFilename; 734 735 memio::concat(&new, s.buf.filename)!; 736 preview = true; 737 }; 738 }; 739 740 let shcmdline = memio::string(&new)!; 741 if (preview) 742 fmt::println(shcmdline)!; 743 744 let rd: io::handle = { 745 let shcmd = exec::cmd("sh", "-c", shcmdline)?; 746 let pipe = exec::pipe(); 747 exec::addfile(&shcmd, os::stdout_file, pipe.1); 748 let proc = exec::start(&shcmd)?; 749 io::close(pipe.1)!; 750 exec::wait(&proc)?; 751 yield pipe.0; 752 }; 753 defer io::close(rd)!; 754 755 io::copy(os::stdout, rd)!; 756 757 if (!s.suppressmode) 758 fmt::println("!")!; 759 760 s.lastshcmd = strings::dup(shcmdline); 761 }; 762 763 fn cmd_null(s: *Session, cmd: *Command) (void | Error) = { 764 s.warned = false; 765 766 const n = get_linenum( 767 cmd.linenums, 768 addr_nextline(s.buf, s.buf.cursor), 769 ); 770 assert_nonzero(s, n)?; 771 772 fmt::println(s.buf.lines[n].text)!; 773 774 s.buf.cursor = n; 775 };