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 };