9fans.net/go@v0.0.7/cmd/sam/sam.go (about) 1 // #include "sam.h" 2 3 // Sam is a multi-file text editor. 4 // This is a Go port of the original C version. 5 // See https://9fans.github.io/plan9port/man/man1/sam.html 6 // and https://9p.io/sys/doc/sam/sam.pdf for details. 7 package main 8 9 import ( 10 "flag" 11 "fmt" 12 "io" 13 "os" 14 "path/filepath" 15 ) 16 17 var genbuf [BLOCKSIZE]rune 18 var genbuf2 [BLOCKSIZE]rune 19 20 var iofile IOFile 21 22 type IOFile interface { 23 io.ReadWriteCloser 24 Stat() (os.FileInfo, error) 25 } 26 27 var panicking int 28 var rescuing int 29 var genstr String 30 var rhs String 31 var curwd String 32 var cmdstr String 33 var empty []rune 34 var curfile *File 35 var flist *File 36 var cmd *File 37 var mainloop int 38 var tempfile []*File 39 var quitok bool = true 40 var downloaded bool 41 var dflag bool 42 var Rflag bool 43 var machine string 44 var home string 45 var bpipeok bool 46 var termlocked int 47 var samterm string = SAMTERM 48 var rsamname string = RSAM 49 var lastfile *File 50 var disk *Disk 51 var seq int 52 53 var winsize string 54 55 var baddir = [9]rune{'<', 'b', 'a', 'd', 'd', 'i', 'r', '>', '\n'} 56 57 /* extern */ 58 59 func main() { 60 var aflag bool 61 var Wflag string 62 63 flag.BoolVar(&Dflag, "D", Dflag, "-D") // debug 64 flag.BoolVar(&dflag, "d", dflag, "-d") 65 flag.BoolVar(&Rflag, "R", Rflag, "-R") 66 flag.StringVar(&machine, "r", machine, "-r") 67 flag.StringVar(&samterm, "t", samterm, "-t") 68 flag.StringVar(&rsamname, "s", rsamname, "-s") 69 flag.BoolVar(&aflag, "a", aflag, "-a (for samterm)") 70 flag.StringVar(&Wflag, "W", Wflag, "-W (for samterm)") 71 72 flag.Usage = usage 73 flag.Parse() 74 75 termargs := []string{"samterm"} 76 if aflag { 77 termargs = append(termargs, "-a") 78 } 79 if Wflag != "" { 80 termargs = append(termargs, "-W", Wflag) 81 } 82 83 Strinit(&cmdstr) 84 Strinit0(&lastpat) 85 Strinit0(&lastregexp) 86 Strinit0(&genstr) 87 Strinit0(&rhs) 88 Strinit0(&curwd) 89 Strinit0(&plan9cmd) 90 home, _ = os.UserHomeDir() 91 disk = diskinit() 92 if home == "" { 93 home = "/" 94 } 95 fileargs := flag.Args() 96 if !dflag { 97 startup(machine, Rflag, termargs, fileargs) 98 } 99 siginit() 100 getcurwd() 101 if len(fileargs) > 0 { 102 for i := 0; i < len(fileargs); i++ { 103 func() { 104 defer func() { 105 e := recover() 106 if e == nil || e == &mainloop { 107 return 108 } 109 panic(e) 110 }() 111 112 t := tmpcstr(fileargs[i]) 113 Strduplstr(&genstr, t) 114 freetmpstr(t) 115 fixname(&genstr) 116 logsetname(newfile(), &genstr) 117 }() 118 } 119 } else if !downloaded { 120 newfile() 121 } 122 seq++ 123 if len(file) > 0 { 124 current(file[0]) 125 } 126 127 for { 128 func() { 129 defer func() { 130 e := recover() 131 if e == nil || e == &mainloop { 132 return 133 } 134 panic(e) 135 }() 136 cmdloop() 137 trytoquit() /* if we already q'ed, quitok will be TRUE */ 138 os.Exit(0) 139 }() 140 } 141 } 142 143 func usage() { 144 dprint("usage: sam [-d] [-t samterm] [-s sam name] [-r machine] [file ...]\n") 145 os.Exit(2) 146 } 147 148 func rescue() { 149 nblank := 0 150 if rescuing++; rescuing > 1 { 151 return 152 } 153 iofile = nil 154 for _, f := range file { 155 if f == cmd || f.b.nc == 0 || !fileisdirty(f) { 156 continue 157 } 158 if iofile == nil { 159 var err error 160 iofile, err = os.OpenFile(filepath.Join(home, "sam.save"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) 161 if err != nil { 162 return 163 } 164 } 165 var buf string 166 if len(f.name.s) > 0 { 167 buf = string(f.name.s) 168 } else { 169 buf = fmt.Sprintf("nameless.%d", nblank) 170 nblank++ 171 } 172 root := os.Getenv("PLAN9") 173 if root == "" { 174 root = "/usr/local/plan9" 175 } 176 fmt.Fprintf(iofile, "#!/bin/sh\n%s/bin/samsave '%s' $* <<'---%s'\n", root, buf, buf) 177 addr.r.p1 = 0 178 addr.r.p2 = f.b.nc 179 writeio(f) 180 fmt.Fprintf(iofile, "\n---%s\n", (string)(buf)) 181 } 182 } 183 184 func panic_(format string, args ...interface{}) { 185 if panicking++; panicking == 1 { 186 s := fmt.Sprintf(format, args...) 187 func() { 188 defer func() { 189 e := recover() 190 if e == nil || e == &mainloop { 191 return 192 } 193 panic(e) 194 }() 195 196 wasd := downloaded 197 downloaded = false 198 dprint("sam: panic: %s\n", s) 199 if wasd { 200 fmt.Fprintf(os.Stderr, "sam: panic: %s\n", s) 201 } 202 rescue() 203 panic("abort") 204 }() 205 } 206 } 207 208 func hiccough(s string) { 209 if rescuing != 0 { 210 os.Exit(1) 211 } 212 if s != "" { 213 dprint("%s\n", s) 214 // panic(s) // TODO(rsc) 215 } 216 resetcmd() 217 resetxec() 218 resetsys() 219 if iofile != nil { 220 iofile.Close() 221 } 222 223 /* 224 * back out any logged changes & restore old sequences 225 */ 226 for _, f := range file { 227 if f == cmd { 228 continue 229 } 230 if f.seq == seq { 231 bufdelete(&f.epsilon, 0, f.epsilon.nc) 232 f.seq = f.prevseq 233 f.dot.r = f.prevdot 234 f.mark = f.prevmark 235 m := Clean 236 if f.prevmod { 237 m = Dirty 238 } 239 state(f, m) 240 } 241 } 242 243 update() 244 if curfile != nil { 245 if curfile.unread { 246 curfile.unread = false 247 } else if downloaded { 248 outTs(Hcurrent, curfile.tag) 249 } 250 } 251 panic(&mainloop) 252 } 253 254 func intr() { 255 error_(Eintr) 256 } 257 258 func trytoclose(f *File) { 259 if f == cmd { /* possible? */ 260 return 261 } 262 if f.deleted { 263 return 264 } 265 if fileisdirty(f) && !f.closeok { 266 f.closeok = true 267 var buf string 268 if len(f.name.s) > 0 { 269 buf = string(f.name.s) 270 } else { 271 buf = "nameless file" 272 } 273 error_s(Emodified, buf) 274 } 275 f.deleted = true 276 } 277 278 func trytoquit() { 279 if !quitok { 280 for _, f := range file { 281 if f != cmd && fileisdirty(f) { 282 quitok = true 283 eof = false 284 error_(Echanges) 285 } 286 } 287 } 288 } 289 290 func load(f *File) { 291 Strduplstr(&genstr, &f.name) 292 filename(f) 293 if len(f.name.s) > 0 { 294 saveaddr := addr 295 edit(f, 'I') 296 addr = saveaddr 297 } else { 298 f.unread = false 299 f.cleanseq = f.seq 300 } 301 302 fileupdate(f, true, true) 303 } 304 305 func cmdupdate() { 306 if cmd != nil && cmd.seq != 0 { 307 fileupdate(cmd, false, downloaded) 308 cmd.dot.r.p2 = cmd.b.nc 309 cmd.dot.r.p1 = cmd.dot.r.p2 310 telldot(cmd) 311 } 312 } 313 314 func delete(f *File) { 315 if downloaded && f.rasp != nil { 316 outTs(Hclose, f.tag) 317 } 318 delfile(f) 319 if f == curfile { 320 current(nil) 321 } 322 } 323 324 func update() { 325 settempfile() 326 anymod := 0 327 for _, f := range tempfile { 328 if f == cmd { /* cmd gets done in main() */ 329 continue 330 } 331 if f.deleted { 332 delete(f) 333 continue 334 } 335 if f.seq == seq && fileupdate(f, false, downloaded) { 336 anymod++ 337 } 338 if f.rasp != nil { 339 telldot(f) 340 } 341 } 342 if anymod != 0 { 343 seq++ 344 } 345 } 346 347 func current(f *File) *File { 348 curfile = f 349 return curfile 350 } 351 352 func edit(f *File, cmd rune) { 353 empty := true 354 if cmd == 'r' { 355 logdelete(f, addr.r.p1, addr.r.p2) 356 } 357 if cmd == 'e' || cmd == 'I' { 358 logdelete(f, Posn(0), f.b.nc) 359 addr.r.p2 = f.b.nc 360 } else if f.b.nc != 0 || (f.name.s != nil && Strcmp(&genstr, &f.name) != 0) { 361 empty = false 362 } 363 var err error 364 iofile, err = os.Open(genc) 365 if err != nil { 366 if curfile != nil && curfile.unread { 367 curfile.unread = false 368 } 369 error_r(Eopen, genc, err) 370 } 371 var nulls bool 372 p := readio(f, &nulls, empty, true) 373 cp := p 374 if cmd == 'e' || cmd == 'I' { 375 cp = -1 376 } 377 closeio(cp) 378 if cmd == 'r' { 379 f.ndot.r.p1 = addr.r.p2 380 f.ndot.r.p2 = addr.r.p2 + p 381 } else { 382 f.ndot.r.p2 = 0 383 f.ndot.r.p1 = f.ndot.r.p2 384 } 385 f.closeok = empty 386 if quitok { 387 quitok = empty 388 } else { 389 quitok = false 390 } 391 m := Clean 392 if !empty && nulls { 393 m = Dirty 394 } 395 state(f, m) 396 if empty && !nulls { 397 f.cleanseq = f.seq 398 } 399 if cmd == 'e' { 400 filename(f) 401 } 402 } 403 404 func getname(f *File, s *String, save bool) int { 405 Strzero(&genstr) 406 genc = "" 407 var c rune 408 if s == nil || len(s.s) == 0 { /* no name provided */ 409 if f != nil { 410 Strduplstr(&genstr, &f.name) 411 } 412 } else { 413 c = s.s[0] 414 if c != ' ' && c != '\t' { 415 error_(Eblank) 416 } 417 var i int 418 for i = 0; i < len(s.s); i++ { 419 c = s.s[i] 420 if !(c == ' ') && !(c == '\t') { 421 break 422 } 423 } 424 for i < len(s.s) && s.s[i] > ' ' { 425 Straddc(&genstr, s.s[i]) 426 i++ 427 } 428 if i != len(s.s) { 429 error_(Enewline) 430 } 431 fixname(&genstr) 432 if f != nil && (save || len(f.name.s) == 0) { 433 logsetname(f, &genstr) 434 if Strcmp(&f.name, &genstr) != 0 { 435 f.closeok = false 436 quitok = f.closeok 437 f.info = nil 438 state(f, Dirty) /* if it's 'e', fix later */ 439 } 440 } 441 } 442 genc = Strtoc(&genstr) 443 return len(genstr.s) 444 } 445 446 func filename(f *File) { 447 genc = string(genstr.s) 448 ch := func(s string, b bool) byte { 449 if b { 450 return s[1] 451 } 452 return s[0] 453 } 454 dprint("%c%c%c %s\n", ch(" '", f.mod), ch("-+", f.rasp != nil), ch(" .", f == curfile), genc) 455 } 456 457 func undostep(f *File, isundo bool) { 458 mod := f.mod 459 var p1 int 460 var p2 int 461 fileundo(f, isundo, true, &p1, &p2, true) 462 f.ndot = f.dot 463 if f.mod { 464 f.closeok = false 465 quitok = false 466 } else { 467 f.closeok = true 468 } 469 470 if f.mod != mod { 471 f.mod = mod 472 m := Clean 473 if mod { 474 m = Dirty 475 } 476 state(f, m) 477 } 478 } 479 480 func undo(isundo bool) int { 481 max := undoseq(curfile, isundo) 482 if max == 0 { 483 return 0 484 } 485 settempfile() 486 for _, f := range tempfile { 487 if f != cmd && undoseq(f, isundo) == max { 488 undostep(f, isundo) 489 } 490 } 491 return 1 492 } 493 494 func readcmd(s *String) int { 495 if flist != nil { 496 fileclose(flist) 497 } 498 flist = fileopen() 499 500 addr.r.p1 = 0 501 addr.r.p2 = flist.b.nc 502 retcode := plan9(flist, '<', s, false) 503 fileupdate(flist, false, false) 504 flist.seq = 0 505 if flist.b.nc > BLOCKSIZE { 506 error_(Etoolong) 507 } 508 Strzero(&genstr) 509 Strinsure(&genstr, flist.b.nc) 510 bufread(&flist.b, Posn(0), genbuf[:flist.b.nc]) 511 copy(genstr.s, genbuf[:]) 512 return retcode 513 } 514 515 func getcurwd() { 516 wd, _ := os.Getwd() 517 t := tmpcstr(wd) 518 Strduplstr(&curwd, t) 519 freetmpstr(t) 520 if len(curwd.s) == 0 { 521 warn(Wpwd) 522 } else if curwd.s[len(curwd.s)-1] != '/' { 523 Straddc(&curwd, '/') 524 } 525 } 526 527 func cd(str *String) { 528 getcurwd() 529 var s string 530 if getname(nil, str, false) != 0 { 531 s = genc 532 } else { 533 s = home 534 } 535 if err := os.Chdir(s); err != nil { 536 syserror("chdir", err) 537 } 538 /* 539 fd := syscall.Open("/dev/wdir", syscall.O_WRONLY) 540 if fd > 0 { 541 write(fd, s, strlen(s)) 542 } 543 */ 544 dprint("!\n") 545 var owd String 546 Strinit(&owd) 547 Strduplstr(&owd, &curwd) 548 getcurwd() 549 settempfile() 550 /* 551 * Two passes so that if we have open 552 * /a/foo.c and /b/foo.c and cd from /b to /a, 553 * we don't ever have two foo.c simultaneously. 554 */ 555 for _, f := range tempfile { 556 if f != cmd && len(f.name.s) > 0 && f.name.s[0] != '/' { 557 Strinsert(&f.name, &owd, Posn(0)) 558 fixname(&f.name) 559 sortname(f) 560 } 561 } 562 for _, f := range tempfile { 563 if f != cmd && Strispre(&curwd, &f.name) { 564 fixname(&f.name) 565 sortname(f) 566 } 567 } 568 Strclose(&owd) 569 } 570 571 func loadflist(s *String) bool { 572 var i int 573 var c rune 574 if len(s.s) > 0 { 575 c = s.s[0] 576 } 577 for i = 0; i < len(s.s) && (s.s[i] == ' ' || s.s[i] == '\t'); i++ { 578 } 579 if (c == ' ' || c == '\t') && (i >= len(s.s) || s.s[i] != '\n') { 580 if i < len(s.s) && s.s[i] == '<' { 581 Strdelete(s, 0, int(i)+1) 582 readcmd(s) 583 } else { 584 Strzero(&genstr) 585 for ; i < len(s.s); i++ { 586 c := s.s[i] 587 if c == '\n' { 588 break 589 } 590 Straddc(&genstr, c) 591 } 592 } 593 } else { 594 if c != '\n' { 595 error_(Eblank) 596 } 597 Strdupl(&genstr, empty) 598 } 599 genc = Strtoc(&genstr) 600 debug("loadflist %s\n", genc) 601 return len(genstr.s) > 0 602 } 603 604 func readflist(readall, delete bool) *File { 605 var t String 606 Strinit(&t) 607 i := 0 608 var f *File 609 for ; f == nil || readall || delete; i++ { /* ++ skips blank */ 610 debug("readflist %q\n", string(genstr.s)) 611 Strdelete(&genstr, Posn(0), i) 612 for i = 0; i < len(genstr.s); i++ { 613 c := genstr.s[i] 614 if c != ' ' && c != '\t' && c != '\n' { 615 break 616 } 617 } 618 if i >= len(genstr.s) { 619 break 620 } 621 Strdelete(&genstr, Posn(0), i) 622 for i = 0; i < len(genstr.s); i++ { 623 c := genstr.s[i] 624 if c == ' ' || c == '\t' || c == '\n' { 625 break 626 } 627 } 628 if i == 0 { 629 break 630 } 631 Strduplstr(&t, tmprstr(genstr.s[:i])) 632 debug("dup %s\n", string(t.s)) 633 fixname(&t) 634 debug("lookfile %s\n", string(t.s)) 635 f = lookfile(&t) 636 if delete { 637 if f == nil { 638 warn_S(Wfile, &t) 639 } else { 640 trytoclose(f) 641 } 642 } else if f == nil && readall { 643 f = newfile() 644 logsetname(f, &t) 645 } 646 } 647 Strclose(&t) 648 return f 649 } 650 651 func tofile(s *String) *File { 652 if s.s[0] != ' ' { 653 error_(Eblank) 654 } 655 var f *File 656 if !loadflist(s) { 657 f = lookfile(&genstr) /* empty string ==> nameless file */ 658 if f == nil { 659 error_s(Emenu, genc) 660 } 661 } else { 662 f = readflist(false, false) 663 if f == nil { 664 error_s(Emenu, genc) 665 } 666 } 667 return current(f) 668 } 669 670 func getfile(s *String) *File { 671 var f *File 672 if !loadflist(s) { 673 f = newfile() 674 logsetname(f, &genstr) 675 } else { 676 debug("read? %q\n", genc) 677 f = readflist(true, false) 678 if f == nil { 679 error_(Eblank) 680 } 681 } 682 return current(f) 683 } 684 685 func closefiles(f *File, s *String) { 686 if len(s.s) == 0 { 687 if f == nil { 688 error_(Enofile) 689 } 690 trytoclose(f) 691 return 692 } 693 if s.s[0] != ' ' { 694 error_(Eblank) 695 } 696 if !loadflist(s) { 697 error_(Enewline) 698 } 699 readflist(false, true) 700 } 701 702 func fcopy(f *File, addr2 Address) { 703 var ni int 704 for p := addr.r.p1; p < addr.r.p2; p += ni { 705 ni = addr.r.p2 - p 706 if ni > BLOCKSIZE { 707 ni = BLOCKSIZE 708 } 709 bufread(&f.b, p, genbuf[:ni]) 710 loginsert(addr2.f, addr2.r.p2, tmprstr(genbuf[:ni]).s) 711 } 712 addr2.f.ndot.r.p2 = addr2.r.p2 + (f.dot.r.p2 - f.dot.r.p1) 713 addr2.f.ndot.r.p1 = addr2.r.p2 714 } 715 716 func move(f *File, addr2 Address) { 717 if addr.r.p2 <= addr2.r.p2 { 718 logdelete(f, addr.r.p1, addr.r.p2) 719 fcopy(f, addr2) 720 } else if addr.r.p1 >= addr2.r.p2 { 721 fcopy(f, addr2) 722 logdelete(f, addr.r.p1, addr.r.p2) 723 } else { 724 error_(Eoverlap) 725 } 726 } 727 728 func nlcount(f *File, p0 Posn, p1 Posn) Posn { 729 nl := 0 730 731 for p0 < p1 { 732 tmp30 := p0 733 p0++ 734 if filereadc(f, tmp30) == '\n' { 735 nl++ 736 } 737 } 738 return nl 739 } 740 741 func printposn(f *File, charsonly bool) { 742 if !charsonly { 743 l1 := 1 + nlcount(f, Posn(0), addr.r.p1) 744 l2 := l1 + nlcount(f, addr.r.p1, addr.r.p2) 745 /* check if addr ends with '\n' */ 746 if addr.r.p2 > 0 && addr.r.p2 > addr.r.p1 && filereadc(f, addr.r.p2-1) == '\n' { 747 l2-- 748 } 749 dprint("%d", l1) 750 if l2 != l1 { 751 dprint(",%d", l2) 752 } 753 dprint("; ") 754 } 755 dprint("#%d", addr.r.p1) 756 if addr.r.p2 != addr.r.p1 { 757 dprint(",#%d", addr.r.p2) 758 } 759 dprint("\n") 760 } 761 762 func settempfile() { 763 tempfile = append(tempfile[:0], file...) 764 }