github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/elvish/edit/edit.go (about) 1 // Package edit implements a command line editor. 2 package edit 3 4 import ( 5 "bufio" 6 "bytes" 7 "fmt" 8 "io" 9 "os" 10 "sync" 11 "syscall" 12 "time" 13 14 "github.com/u-root/u-root/cmds/elvish/edit/eddefs" 15 "github.com/u-root/u-root/cmds/elvish/edit/highlight" 16 "github.com/u-root/u-root/cmds/elvish/edit/tty" 17 "github.com/u-root/u-root/cmds/elvish/edit/ui" 18 "github.com/u-root/u-root/cmds/elvish/eval" 19 "github.com/u-root/u-root/cmds/elvish/eval/vals" 20 "github.com/u-root/u-root/cmds/elvish/parse" 21 "github.com/u-root/u-root/cmds/elvish/sys" 22 "github.com/u-root/u-root/cmds/elvish/util" 23 "github.com/u-root/u-root/cmds/elvish/hashmap" 24 ) 25 26 var logger = util.GetLogger("[edit] ") 27 28 // editor implements the line editor. 29 type editor struct { 30 in *os.File 31 out *os.File 32 writer tty.Writer 33 reader tty.Reader 34 sigs <-chan os.Signal 35 evaler *eval.Evaler 36 37 active bool 38 activeMutex sync.Mutex 39 40 // notifyPort is a write-only port that turns data written to it into editor 41 // notifications. 42 notifyPort *eval.Port 43 // notifyRead is the read end of notifyPort.File. 44 notifyRead *os.File 45 46 // Configurations. Each of the following fields have an initializer defined 47 // using atEditorInit. 48 editorHooks 49 abbr hashmap.Map 50 argCompleter hashmap.Map 51 maxHeight float64 52 53 prompt, rprompt eddefs.Prompt 54 RpromptPersistent bool 55 56 // Modes. 57 insert *insert 58 command *command 59 navigation *navigation 60 listing *listingMode 61 62 editorState 63 } 64 65 type editorState struct { 66 // States used during ReadLine. Reset at the beginning of ReadLine. 67 restoreTerminal func() error 68 69 notificationMutex sync.Mutex 70 71 notifications []string 72 tips []string 73 74 buffer string 75 dot int 76 77 chunk *parse.Chunk 78 styling *highlight.Styling 79 parseErrorAtEnd bool 80 81 promptContent, rpromptContent []*ui.Styled 82 83 mode eddefs.Mode 84 85 // A cache of external commands, used in stylist. 86 isExternal map[string]bool 87 88 // Used for builtins. 89 lastKey ui.Key 90 nextAction eddefs.Action 91 } 92 93 // NewEditor creates an Editor. When the instance is no longer used, its Close 94 // method should be called. 95 func NewEditor(in *os.File, out *os.File, sigs <-chan os.Signal, ev *eval.Evaler) eddefs.Editor { 96 ed := &editor{ 97 in: in, 98 out: out, 99 writer: tty.NewWriter(out), 100 reader: tty.NewReader(in), 101 sigs: sigs, 102 evaler: ev, 103 } 104 105 notifyChan := make(chan interface{}) 106 notifyRead, notifyWrite, err := os.Pipe() 107 if err != nil { 108 panic(err) 109 } 110 ed.notifyPort = &eval.Port{File: notifyWrite, Chan: notifyChan} 111 ed.notifyRead = notifyRead 112 // Forward reads from notifyRead to notification. 113 go func() { 114 reader := bufio.NewReader(notifyRead) 115 for { 116 line, err := reader.ReadString('\n') 117 if err != nil { 118 break 119 } 120 ed.Notify("[bytes out] %s", line[:len(line)-1]) 121 } 122 if err != io.EOF { 123 logger.Println("notifyRead error:", err) 124 } 125 }() 126 // Forward reads from notifyChan to notification. 127 go func() { 128 for v := range notifyChan { 129 ed.Notify("[value out] %s", vals.Repr(v, vals.NoPretty)) 130 } 131 }() 132 133 ev.Editor = ed 134 135 ns := makeNs(ed) 136 for _, f := range editorInitFuncs { 137 f(ed, ns) 138 } 139 ev.Builtin.AddNs("edit", ns) 140 141 err = ev.EvalSource(eval.NewScriptSource("[editor]", "[editor]", "use binding; binding:install")) 142 if err != nil { 143 fmt.Fprintln(out, "Failed to load default binding:", err) 144 } 145 146 return ed 147 } 148 149 func (ed *editor) Close() { 150 ed.reader.Close() 151 close(ed.notifyPort.Chan) 152 ed.notifyPort.File.Close() 153 ed.notifyRead.Close() 154 ed.prompt.Close() 155 ed.rprompt.Close() 156 } 157 158 func (ed *editor) Evaler() *eval.Evaler { 159 return ed.evaler 160 } 161 162 func (ed *editor) Buffer() (string, int) { 163 return ed.buffer, ed.dot 164 } 165 166 func (ed *editor) SetBuffer(buffer string, dot int) { 167 ed.buffer, ed.dot = buffer, dot 168 } 169 170 func (ed *editor) ParsedBuffer() *parse.Chunk { 171 return ed.chunk 172 } 173 174 func (ed *editor) SetMode(m eddefs.Mode) { 175 if ed.mode != nil { 176 ed.mode.Teardown() 177 } 178 ed.mode = m 179 } 180 181 func (ed *editor) SetModeInsert() { 182 ed.SetMode(ed.insert) 183 } 184 185 func (ed *editor) SetModeListing(b eddefs.BindingMap, p eddefs.ListingProvider) { 186 ed.listing.setup(b, p) 187 ed.SetMode(ed.listing) 188 } 189 190 func (ed *editor) RefreshListing() { 191 if l, ok := ed.mode.(*listingMode); ok { 192 l.refresh() 193 } 194 } 195 196 func (ed *editor) flash() { 197 // TODO implement fish-like flash effect 198 } 199 200 // AddTip adds a message to the tip area. 201 func (ed *editor) AddTip(format string, args ...interface{}) { 202 ed.tips = append(ed.tips, fmt.Sprintf(format, args...)) 203 } 204 205 // Notify writes out a message in a way that does not interrupt the editor 206 // display. When the editor is not active, it simply writes the message to the 207 // terminal. When the editor is active, it appends the message to the 208 // notification queue, which will be written out during the update cycle. It can 209 // be safely used concurrently. 210 func (ed *editor) Notify(format string, args ...interface{}) { 211 msg := fmt.Sprintf(format, args...) 212 ed.activeMutex.Lock() 213 defer ed.activeMutex.Unlock() 214 // If the editor is not active, simply write out the message. 215 if !ed.active { 216 ed.out.WriteString(msg + "\n") 217 return 218 } 219 ed.notificationMutex.Lock() 220 defer ed.notificationMutex.Unlock() 221 ed.notifications = append(ed.notifications, msg) 222 } 223 224 func (ed *editor) LastKey() ui.Key { 225 return ed.lastKey 226 } 227 228 func (ed *editor) refresh(fullRefresh bool, addErrorsToTips bool) error { 229 src := ed.buffer 230 // Parse the current line 231 n, err := parse.Parse("[interactive]", src) 232 ed.chunk = n 233 234 ed.parseErrorAtEnd = err != nil && atEnd(err, len(src)) 235 // If all parse errors are at the end, it is likely caused by incomplete 236 // input. In that case, do not complain about parse errors. 237 // TODO(xiaq): Find a more reliable way to determine incomplete input. 238 // Ideally the parser should report it. 239 if err != nil && addErrorsToTips && !ed.parseErrorAtEnd { 240 ed.AddTip("%s", err) 241 } 242 243 ed.styling = &highlight.Styling{} 244 doHighlight(n, ed) 245 246 _, err = ed.evaler.Compile(n, eval.NewInteractiveSource(src)) 247 if err != nil && !atEnd(err, len(src)) { 248 if addErrorsToTips { 249 ed.AddTip("%s", err) 250 } 251 // Highlight errors in the input buffer. 252 ctx := err.(*eval.CompilationError).Context 253 ed.styling.Add(ctx.Begin, ctx.End, styleForCompilerError.String()) 254 } 255 256 // Render onto a buffer. 257 height, width := sys.GetWinsize(ed.out) 258 height = min(height, maxHeightToInt(ed.maxHeight)) 259 er := &editorRenderer{&ed.editorState, height, nil} 260 buf := ui.Render(er, width) 261 return ed.writer.CommitBuffer(er.bufNoti, buf, fullRefresh) 262 } 263 264 func atEnd(e error, n int) bool { 265 switch e := e.(type) { 266 case *eval.CompilationError: 267 return e.Context.Begin == n 268 case *parse.Error: 269 for _, entry := range e.Entries { 270 if entry.Context.Begin != n { 271 return false 272 } 273 } 274 return true 275 default: 276 logger.Printf("atEnd called with error type %T", e) 277 return false 278 } 279 } 280 281 // InsertAtDot inserts text at the dot and moves the dot after it. 282 func (ed *editor) InsertAtDot(text string) { 283 ed.buffer = ed.buffer[:ed.dot] + text + ed.buffer[ed.dot:] 284 ed.dot += len(text) 285 } 286 287 func (ed *editor) SetPrompt(prompt eddefs.Prompt) { 288 ed.prompt = prompt 289 } 290 291 func (ed *editor) SetRPrompt(rprompt eddefs.Prompt) { 292 ed.rprompt = rprompt 293 } 294 295 // startReadLine prepares the terminal for the editor. 296 func (ed *editor) startReadLine() error { 297 ed.activeMutex.Lock() 298 defer ed.activeMutex.Unlock() 299 ed.active = true 300 301 restoreTerminal, err := tty.Setup(ed.in, ed.out) 302 if err != nil { 303 if restoreTerminal != nil { 304 restoreTerminal() 305 } 306 return err 307 } 308 ed.restoreTerminal = restoreTerminal 309 310 return nil 311 } 312 313 // finishReadLine puts the terminal in a state suitable for other programs to 314 // use. 315 func (ed *editor) finishReadLine() error { 316 // After-readline hooks should be called before most teardown happens as 317 // they can cause the editor to refresh. 318 for _, f := range ed.afterReadline { 319 f(ed.buffer) 320 } 321 322 ed.activeMutex.Lock() 323 defer ed.activeMutex.Unlock() 324 ed.active = false 325 326 // Refresh the terminal for the last time in a clean-ish state. 327 ed.SetModeInsert() 328 ed.tips = nil 329 ed.dot = len(ed.buffer) 330 if !ed.RpromptPersistent { 331 ed.rpromptContent = nil 332 } 333 errRefresh := ed.refresh(false, false) 334 ed.mode.Teardown() 335 ed.out.WriteString("\n") 336 ed.writer.ResetCurrentBuffer() 337 338 ed.reader.Stop() 339 340 // Restore termios. 341 errRestore := ed.restoreTerminal() 342 343 // Reset all of editorState. 344 ed.editorState = editorState{} 345 346 return util.Errors(errRefresh, errRestore) 347 } 348 349 // ReadLine reads a line interactively. 350 func (ed *editor) ReadLine() (string, error) { 351 err := ed.startReadLine() 352 if err != nil { 353 return "", err 354 } 355 defer func() { 356 err := ed.finishReadLine() 357 if err != nil { 358 fmt.Fprintln(ed.out, "error:", err) 359 } 360 }() 361 362 ed.SetModeInsert() 363 364 // Find external commands asynchronously, so that slow I/O won't block the 365 // editor. 366 isExternalCh := make(chan map[string]bool, 1) 367 go getIsExternal(ed.evaler, isExternalCh) 368 369 ed.reader.Start() 370 371 fullRefresh := false 372 373 for _, f := range ed.beforeReadline { 374 f() 375 } 376 377 ed.promptContent = ed.prompt.Last() 378 ed.rpromptContent = ed.rprompt.Last() 379 fresh := true 380 MainLoop: 381 for { 382 ed.prompt.Update(fresh) 383 ed.rprompt.Update(fresh) 384 fresh = false 385 386 refresh: 387 err := ed.refresh(fullRefresh, true) 388 fullRefresh = false 389 if err != nil { 390 return "", err 391 } 392 393 ed.tips = nil 394 395 select { 396 case ed.promptContent = <-ed.prompt.Chan(): 397 logger.Println("prompt fetched late") 398 goto refresh 399 case ed.rpromptContent = <-ed.rprompt.Chan(): 400 logger.Println("rprompt fetched late") 401 goto refresh 402 case m := <-isExternalCh: 403 ed.isExternal = m 404 goto refresh 405 case sig := <-ed.sigs: 406 // TODO(xiaq): Maybe support customizable handling of signals 407 switch sig { 408 case syscall.SIGHUP: 409 return "", io.EOF 410 case syscall.SIGINT: 411 // Start over 412 ed.mode.Teardown() 413 ed.editorState = editorState{ 414 restoreTerminal: ed.restoreTerminal, 415 isExternal: ed.isExternal, 416 } 417 ed.SetModeInsert() 418 fresh = true 419 continue MainLoop 420 case sys.SIGWINCH: 421 fullRefresh = true 422 continue MainLoop 423 default: 424 ed.AddTip("ignored signal %s", sig) 425 } 426 case event := <-ed.reader.EventChan(): 427 switch event := event.(type) { 428 case tty.NonfatalErrorEvent: 429 ed.Notify("error when reading terminal: %v", event.Err) 430 case tty.FatalErrorEvent: 431 ed.Notify("fatal error when reading terminal: %v", event.Err) 432 return "", event.Err 433 case tty.MouseEvent: 434 ed.AddTip("mouse: %+v", event) 435 case tty.CursorPosition: 436 // Ignore CPR 437 case tty.PasteSetting: 438 if !event { 439 continue 440 } 441 var buf bytes.Buffer 442 timer := time.NewTimer(tty.DefaultSeqTimeout) 443 paste: 444 for { 445 // XXX Should also select on other chans. However those chans 446 // will be unified (again) into one later so we don't do 447 // busywork here. 448 select { 449 case event := <-ed.reader.EventChan(): 450 switch event := event.(type) { 451 case tty.KeyEvent: 452 k := ui.Key(event) 453 if k.Mod != 0 { 454 ed.Notify("function key within paste, aborting") 455 break paste 456 } 457 buf.WriteRune(k.Rune) 458 timer.Reset(tty.DefaultSeqTimeout) 459 case tty.PasteSetting: 460 if !event { 461 break paste 462 } 463 default: // Ignore other things. 464 } 465 case <-timer.C: 466 ed.Notify("bracketed paste timeout") 467 break paste 468 } 469 } 470 topaste := buf.String() 471 if ed.insert.quotePaste { 472 topaste = parse.Quote(topaste) 473 } 474 ed.InsertAtDot(topaste) 475 case tty.RawRune: 476 insertRaw(ed, rune(event)) 477 case tty.KeyEvent: 478 k := ui.Key(event) 479 lookupKey: 480 fn := ed.mode.Binding(k) 481 if fn == nil { 482 ed.AddTip("Unbound and no default binding: %s", k) 483 continue MainLoop 484 } 485 486 ed.insert.insertedLiteral = false 487 ed.lastKey = k 488 ed.CallFn(fn) 489 if ed.insert.insertedLiteral { 490 ed.insert.literalInserts++ 491 } else { 492 ed.insert.literalInserts = 0 493 } 494 495 switch ed.popAction() { 496 case reprocessKey: 497 err := ed.refresh(false, true) 498 if err != nil { 499 return "", err 500 } 501 goto lookupKey 502 case commitLine: 503 return ed.buffer, nil 504 case commitEOF: 505 return "", io.EOF 506 } 507 } 508 } 509 } 510 } 511 512 // getIsExternal finds a set of all external commands and puts it on the result 513 // channel. 514 func getIsExternal(ev *eval.Evaler, result chan<- map[string]bool) { 515 isExternal := make(map[string]bool) 516 eval.EachExternal(func(name string) { 517 isExternal[name] = true 518 }) 519 result <- isExternal 520 }