9fans.net/go@v0.0.7/cmd/acme/internal/ui/look.go (about) 1 // #include <u.h> 2 // #include <libc.h> 3 // #include <draw.h> 4 // #include <thread.h> 5 // #include <cursor.h> 6 // #include <mouse.h> 7 // #include <keyboard.h> 8 // #include <frame.h> 9 // #include <fcall.h> 10 // #include <regexp.h> 11 // #include <9pclient.h> 12 // #include <plumb.h> 13 // #include <libsec.h> 14 // #include "dat.h" 15 // #include "fns.h" 16 17 package ui 18 19 import ( 20 "fmt" 21 "os" 22 "path" 23 24 "9fans.net/go/cmd/acme/internal/addr" 25 "9fans.net/go/cmd/acme/internal/adraw" 26 "9fans.net/go/cmd/acme/internal/alog" 27 "9fans.net/go/cmd/acme/internal/bufs" 28 "9fans.net/go/cmd/acme/internal/runes" 29 "9fans.net/go/cmd/acme/internal/util" 30 "9fans.net/go/cmd/acme/internal/wind" 31 "9fans.net/go/draw" 32 "9fans.net/go/plan9/client" 33 "9fans.net/go/plumb" 34 ) 35 36 func Look3(t *wind.Text, q0, q1 int, external bool) { 37 ct := wind.Seltext 38 if ct == nil { 39 wind.Seltext = t 40 } 41 var e Expand 42 expanded := Expand_(t, q0, q1, &e) 43 var n int 44 var c rune 45 var r []rune 46 if !external && t.W != nil && t.W.External { 47 // send alphanumeric expansion to external client 48 if !expanded { 49 return 50 } 51 f := 0 52 if (e.Arg != nil && t.W != nil) || (len(e.Name) > 0 && LookFile(e.Name) != nil) { 53 f = 1 // acme can do it without loading a file 54 } 55 if q0 != e.Q0 || q1 != e.Q1 { 56 f |= 2 // second (post-expand) message follows 57 } 58 if len(e.Name) != 0 { 59 f |= 4 // it's a file name 60 } 61 c = 'l' 62 if t.What == wind.Body { 63 c = 'L' 64 } 65 n = q1 - q0 66 if n <= wind.EVENTSIZE { 67 r := make([]rune, n) 68 t.File.Read(q0, r) 69 wind.Winevent(t.W, "%c%d %d %d %d %s\n", c, q0, q1, f, n, string(r)) 70 } else { 71 wind.Winevent(t.W, "%c%d %d %d 0 \n", c, q0, q1, f) 72 } 73 if q0 == e.Q0 && q1 == e.Q1 { 74 return 75 } 76 if len(e.Name) != 0 { 77 n = len(e.Name) 78 if e.A1 > e.A0 { 79 n += 1 + (e.A1 - e.A0) 80 } 81 r = make([]rune, n) 82 copy(r, e.Name) 83 if e.A1 > e.A0 { 84 r[len(e.Name)] = ':' 85 at := e.Arg.(*wind.Text) 86 at.File.Read(e.A0, r[len(e.Name)+1:]) 87 } 88 } else { 89 n = e.Q1 - e.Q0 90 r = make([]rune, n) 91 t.File.Read(e.Q0, r) 92 } 93 f &^= 2 94 if n <= wind.EVENTSIZE { 95 r := r 96 if len(r) > n { 97 r = r[:n] 98 } 99 wind.Winevent(t.W, "%c%d %d %d %d %s\n", c, e.Q0, e.Q1, f, n, string(r)) 100 } else { 101 wind.Winevent(t.W, "%c%d %d %d 0 \n", c, e.Q0, e.Q1, f) 102 } 103 return 104 } 105 if Plumbsendfid != nil { 106 // send whitespace-delimited word to plumber 107 m := new(plumb.Message) 108 m.Src = "acme" 109 dir := wind.Dirname(t, nil) 110 if len(dir) == 1 && dir[0] == '.' { // sigh 111 dir = nil 112 } 113 if len(dir) == 0 { 114 m.Dir = Wdir 115 } else { 116 m.Dir = string(dir) 117 } 118 m.Type = "text" 119 if q1 == q0 { 120 if t.Q1 > t.Q0 && t.Q0 <= q0 && q0 <= t.Q1 { 121 q0 = t.Q0 122 q1 = t.Q1 123 } else { 124 p := q0 125 for q0 > 0 && func() bool { c = tgetc(t, q0-1); return c != ' ' }() && c != '\t' && c != '\n' { 126 q0-- 127 } 128 for q1 < t.Len() && func() bool { c = tgetc(t, q1); return c != ' ' }() && c != '\t' && c != '\n' { 129 q1++ 130 } 131 if q1 == q0 { 132 return 133 } 134 m.Attr = &plumb.Attribute{Name: "click", Value: fmt.Sprint(p - q0)} 135 } 136 } 137 r = make([]rune, q1-q0) 138 t.File.Read(q0, r) 139 m.Data = []byte(string(r)) 140 if len(m.Data) < 7*1024 && m.Send(Plumbsendfid) == nil { 141 return 142 } 143 // plumber failed to match; fall through 144 } 145 146 // interpret alphanumeric string ourselves 147 if !expanded { 148 return 149 } 150 if e.Name != nil || e.Arg != nil { 151 Openfile(t, &e) 152 } else { 153 if t.W == nil { 154 return 155 } 156 ct = &t.W.Body 157 if t.W != ct.W { 158 wind.Winlock(ct.W, 'M') 159 } 160 if t == ct { 161 wind.Textsetselect(ct, e.Q1, e.Q1) 162 } 163 r = make([]rune, e.Q1-e.Q0) 164 t.File.Read(e.Q0, r) 165 if Search(ct, r) && e.Jump { 166 adraw.Display.MoveCursor(ct.Fr.PointOf(ct.Fr.P0).Add(draw.Pt(4, ct.Fr.Font.Height-4))) 167 } 168 if t.W != ct.W { 169 wind.Winunlock(ct.W) 170 } 171 } 172 } 173 174 func Search(ct *wind.Text, r []rune) bool { 175 if len(r) == 0 || len(r) > ct.Len() { 176 return false 177 } 178 if 2*len(r) > bufs.RuneLen { 179 alog.Printf("string too long\n") // TODO(rsc): why??????? 180 return false 181 } 182 maxn := util.Max(2*len(r), bufs.RuneLen) 183 s := bufs.AllocRunes() 184 b := s[:0] 185 around := 0 186 q := ct.Q1 187 for { 188 if q >= ct.Len() { 189 q = 0 190 around = 1 191 b = b[:0] 192 } 193 if len(b) > 0 { 194 i := runes.IndexRune(b, r[0]) 195 if i < 0 { 196 q += len(b) 197 b = b[:0] 198 if around != 0 && q >= ct.Q1 { 199 break 200 } 201 continue 202 } 203 q += i 204 b = b[i:] 205 } 206 // reload if buffer covers neither string nor rest of file 207 if len(b) < len(r) && len(b) != ct.Len()-q { 208 nb := ct.Len() - q 209 if nb >= maxn { 210 nb = maxn - 1 211 } 212 ct.File.Read(q, s[:nb]) 213 b = s[:nb] 214 } 215 // this runeeq is fishy but the null at b[nb] makes it safe // TODO(rsc): NUL done gone 216 if len(b) >= len(r) && runes.Equal(b[:len(r)], r) { 217 if ct.W != nil { 218 wind.Textshow(ct, q, q+len(r), true) 219 wind.Winsettag(ct.W) 220 } else { 221 ct.Q0 = q 222 ct.Q1 = q + len(r) 223 } 224 wind.Seltext = ct 225 bufs.FreeRunes(s) 226 return true 227 } 228 b = b[1:] 229 q++ 230 if around != 0 && q >= ct.Q1 { 231 break 232 } 233 } 234 bufs.FreeRunes(s) 235 return false 236 } 237 238 // Runestr wrapper for cleanname 239 240 var includefile_Lslash = [2]rune{'/', 0} 241 242 func includefile(dir []rune, file []rune) []rune { 243 a := fmt.Sprintf("%s/%s", string(dir), string(file)) 244 if _, err := os.Stat(a); err != nil { 245 return nil 246 } 247 return []rune(path.Clean(a)) 248 } 249 250 var objdir []rune 251 252 func includename(t *wind.Text, r []rune) []rune { 253 var i int 254 if objdir == nil && Objtype != "" { 255 buf := fmt.Sprintf("/%s/include", Objtype) 256 objdir = []rune(buf) 257 } 258 259 w := t.W 260 if len(r) == 0 || r[0] == '/' || w == nil { 261 return r 262 } 263 if len(r) > 2 && r[0] == '.' && r[1] == '/' { 264 return r 265 } 266 var file []rune 267 file = nil 268 for i = 0; i < len(w.Incl) && file == nil; i++ { 269 file = includefile(w.Incl[i], r) 270 } 271 272 if file == nil { 273 file = includefile([]rune("/sys/include"), r) 274 } 275 if file == nil { 276 file = includefile([]rune("/usr/local/plan9/include"), r) 277 } 278 if file == nil { 279 file = includefile([]rune("/usr/local/include"), r) 280 } 281 if file == nil { 282 file = includefile([]rune("/usr/include"), r) 283 } 284 if file == nil && objdir != nil { 285 file = includefile(objdir, r) 286 } 287 if file == nil { 288 return r 289 } 290 return file 291 } 292 293 func texthas(t *wind.Text, q0 int, r []rune) bool { 294 if int(q0) < 0 { 295 return false 296 } 297 for i := 0; i < len(r); i++ { 298 if q0+i >= t.Len() || t.RuneAt(q0+i) != r[i] { 299 return false 300 } 301 } 302 return true 303 } 304 305 func hasPrefix(r []rune, s []rune) bool { 306 if len(r) < len(s) { 307 return false 308 } 309 for i := 0; i < len(s); i++ { 310 if r[i] != s[i] { 311 return false 312 } 313 } 314 return true 315 } 316 317 func expandfile(t *wind.Text, q0 int, q1 int, e *Expand) bool { 318 amax := q1 319 var c rune 320 if q1 == q0 { 321 colon := -1 322 for q1 < t.Len() { 323 c = t.RuneAt(q1) 324 if !runes.IsFilename(c) { 325 break 326 } 327 if c == ':' && !texthas(t, q1-4, []rune("http://")) && !texthas(t, q1-5, []rune("https://")) { 328 colon = q1 329 break 330 } 331 q1++ 332 } 333 for q0 > 0 { 334 c = t.RuneAt(q0 - 1) 335 if !runes.IsFilename(c) && !runes.IsAddr(c) && !runes.IsRegx(c) { 336 break 337 } 338 q0-- 339 if colon < 0 && c == ':' && !texthas(t, q0-4, []rune("http://")) && !texthas(t, q0-5, []rune("https://")) { 340 colon = q0 341 } 342 } 343 /* 344 * if it looks like it might begin file: , consume address chars after : 345 * otherwise terminate expansion at : 346 */ 347 if colon >= 0 { 348 q1 = colon 349 if colon < t.Len()-1 && runes.IsAddr(t.RuneAt(colon+1)) { 350 q1 = colon + 1 351 for q1 < t.Len() && runes.IsAddr(t.RuneAt(q1)) { 352 q1++ 353 } 354 } 355 } 356 if q1 > q0 { 357 if colon >= 0 { // stop at white space 358 for amax = colon + 1; amax < t.Len(); amax++ { 359 c = t.RuneAt(amax) 360 if c == ' ' || c == '\t' || c == '\n' { 361 break 362 } 363 } 364 } else { 365 amax = t.Len() 366 } 367 } 368 } 369 amin := amax 370 e.Q0 = q0 371 e.Q1 = q1 372 n := q1 - q0 373 if n == 0 { 374 return false 375 } 376 // see if it's a file name 377 r := make([]rune, n) 378 t.File.Read(q0, r) 379 // is it a URL? look for http:// and https:// prefix 380 if hasPrefix(r, []rune("http://")) || hasPrefix(r, []rune("https://")) { 381 // Avoid capturing end-of-sentence punctuation. 382 if r[n-1] == '.' { 383 e.Q1-- 384 n-- 385 } 386 e.Name = r 387 e.Arg = t 388 e.A0 = e.Q1 389 e.A1 = e.Q1 390 return true 391 } 392 // first, does it have bad chars? 393 nname := -1 394 var i int 395 for i = 0; i < n; i++ { 396 c = r[i] 397 if c == ':' && nname < 0 { 398 if q0+i+1 < t.Len() && (i == n-1 || runes.IsAddr(t.RuneAt(q0+i+1))) { 399 amin = q0 + i 400 } else { 401 return false 402 } 403 nname = i 404 } 405 } 406 if nname == -1 { 407 nname = n 408 } 409 for i = 0; i < nname; i++ { 410 if !runes.IsFilename(r[i]) && r[i] != ' ' { 411 return false 412 } 413 } 414 /* 415 * See if it's a file name in <>, and turn that into an include 416 * file name if so. Should probably do it for "" too, but that's not 417 * restrictive enough syntax and checking for a #include earlier on the 418 * line would be silly. 419 */ 420 if q0 > 0 && t.RuneAt(q0-1) == '<' && q1 < t.Len() && t.RuneAt(q1) == '>' { 421 rs := includename(t, r[:nname]) 422 r = rs 423 nname = len(rs) 424 } else if amin == q0 { 425 goto Isfile 426 } else { 427 rs := wind.Dirname(t, r[:nname]) 428 r = rs 429 nname = len(rs) 430 } 431 e.Bname = string(r[:nname]) 432 // if it's already a window name, it's a file 433 { 434 w := LookFile(r[:nname]) 435 if w != nil { 436 goto Isfile 437 } 438 // if it's the name of a file, it's a file 439 if Ismtpt(e.Bname) { 440 e.Bname = "" 441 return false 442 } 443 if _, err := os.Stat(e.Bname); err != nil { 444 e.Bname = "" 445 return false 446 } 447 } 448 449 Isfile: 450 e.Name = r[:nname] 451 e.Arg = t 452 e.A0 = amin + 1 453 eval := false 454 addr.Eval(true, nil, runes.Rng(-1, -1), runes.Rng(0, 0), t, e.A0, amax, tgetc, &eval, (*int)(&e.A1)) 455 return true 456 } 457 458 func Expand_(t *wind.Text, q0 int, q1 int, e *Expand) bool { 459 *e = Expand{} 460 e.Agetc = tgetc 461 // if in selection, choose selection 462 e.Jump = true 463 if q1 == q0 && t.Q1 > t.Q0 && t.Q0 <= q0 && q0 <= t.Q1 { 464 q0 = t.Q0 465 q1 = t.Q1 466 if t.What == wind.Tag { 467 e.Jump = false 468 } 469 } 470 471 if expandfile(t, q0, q1, e) { 472 return true 473 } 474 475 if q0 == q1 { 476 for q1 < t.Len() && runes.IsAlphaNum(t.RuneAt(q1)) { 477 q1++ 478 } 479 for q0 > 0 && runes.IsAlphaNum(t.RuneAt(q0-1)) { 480 q0-- 481 } 482 } 483 e.Q0 = q0 484 e.Q1 = q1 485 return q1 > q0 486 } 487 488 func LookFile(s []rune) *wind.Window { 489 // avoid terminal slash on directories 490 if len(s) > 0 && s[len(s)-1] == '/' { 491 s = s[:len(s)-1] 492 } 493 for _, c := range wind.TheRow.Col { 494 for _, w := range c.W { 495 t := &w.Body 496 k := len(t.File.Name()) 497 if k > 1 && t.File.Name()[k-1] == '/' { 498 k-- 499 } 500 if runes.Equal(t.File.Name()[:k], s) { 501 w = w.Body.File.Curtext.W 502 if w.Col != nil { // protect against race deleting w 503 return w 504 } 505 } 506 } 507 } 508 return nil 509 } 510 511 func LookID(id int) *wind.Window { 512 for _, c := range wind.TheRow.Col { 513 for _, w := range c.W { 514 if w.ID == id { 515 return w 516 } 517 } 518 } 519 return nil 520 } 521 522 type Expand struct { 523 Q0 int 524 Q1 int 525 Name []rune 526 Bname string 527 Jump bool 528 Arg interface{} 529 Agetc func(interface{}, int) rune 530 A0 int 531 A1 int 532 } 533 534 func tgetc(a interface{}, n int) rune { 535 t := a.(*wind.Text) 536 if n >= t.Len() { 537 return 0 538 } 539 return t.RuneAt(n) 540 } 541 542 var Ismtpt func(string) bool 543 544 var Plumbsendfid *client.Fid 545 546 var Wdir = "." 547 548 var Objtype string 549 550 func Openfile(t *wind.Text, e *Expand) *wind.Window { 551 var r runes.Range 552 r.Pos = 0 553 r.End = 0 554 var w *wind.Window 555 if len(e.Name) == 0 { 556 w = t.W 557 if w == nil { 558 return nil 559 } 560 } else { 561 w = LookFile(e.Name) 562 if w == nil && e.Name[0] != '/' { 563 /* 564 * Unrooted path in new window. 565 * This can happen if we type a pwd-relative path 566 * in the topmost tag or the column tags. 567 * Most of the time plumber takes care of these, 568 * but plumber might not be running or might not 569 * be configured to accept plumbed directories. 570 * Make the name a full path, just like we would if 571 * opening via the plumber. 572 */ 573 rs := []rune(path.Join(string(Wdir), string(e.Name))) 574 e.Name = rs 575 w = LookFile(e.Name) 576 } 577 } 578 if w != nil { 579 t = &w.Body 580 if !t.Col.Safe && t.Fr.MaxLines == 0 { // window is obscured by full-column window 581 wind.Colgrow(t.Col, t.Col.W[0], 1) 582 } 583 } else { 584 var ow *wind.Window 585 if t != nil { 586 ow = t.W 587 } 588 w = Makenewwindow(t) 589 t = &w.Body 590 wind.Winsetname(w, e.Name) 591 if Textload(t, 0, e.Bname, true) >= 0 { 592 t.File.Unread = false 593 } 594 t.File.SetMod(false) 595 t.W.Dirty = false 596 wind.Winsettag(t.W) 597 wind.Textsetselect(&t.W.Tag, t.W.Tag.Len(), t.W.Tag.Len()) 598 if ow != nil { 599 for i := len(ow.Incl); ; { 600 i-- 601 if i < 0 { 602 break 603 } 604 rp := runes.Clone(ow.Incl[i]) 605 wind.Winaddincl(w, rp) 606 } 607 w.Autoindent = ow.Autoindent 608 } else { 609 w.Autoindent = wind.GlobalAutoindent 610 } 611 OnNewWindow(w) 612 } 613 var eval bool 614 if e.A1 == e.A0 { 615 eval = false 616 } else { 617 eval = true 618 var dummy int 619 r = addr.Eval(true, t, runes.Rng(-1, -1), runes.Rng(t.Q0, t.Q1), e.Arg, e.A0, e.A1, e.Agetc, &eval, &dummy) 620 if r.Pos > r.End { 621 eval = false 622 alog.Printf("addresses out of order\n") 623 } 624 if !eval { 625 e.Jump = false // don't jump if invalid address 626 } 627 } 628 if !eval { 629 r.Pos = t.Q0 630 r.End = t.Q1 631 } 632 wind.Textshow(t, r.Pos, r.End, true) 633 wind.Winsettag(t.W) 634 wind.Seltext = t 635 if e.Jump { 636 adraw.Display.MoveCursor(t.Fr.PointOf(t.Fr.P0).Add(draw.Pt(4, adraw.Font.Height-4))) 637 } 638 return w 639 } 640 641 var Textload func(*wind.Text, int, string, bool) int 642 643 var OnNewWindow func(*wind.Window)