github.com/hoop33/elvish@v0.0.0-20160801152013-6d25485beab4/edit/editor.go (about) 1 // Package edit implements a command line editor. 2 package edit 3 4 import ( 5 "bytes" 6 "fmt" 7 "os" 8 "syscall" 9 "time" 10 11 "github.com/elves/elvish/eval" 12 "github.com/elves/elvish/parse" 13 "github.com/elves/elvish/store" 14 "github.com/elves/elvish/sys" 15 "github.com/elves/elvish/util" 16 ) 17 18 var Logger = util.GetLogger("[edit] ") 19 20 const ( 21 lackEOLRune = '\u23ce' 22 lackEOL = "\033[7m" + string(lackEOLRune) + "\033[m" 23 ) 24 25 // Editor keeps the status of the line editor. 26 type Editor struct { 27 file *os.File 28 writer *writer 29 reader *Reader 30 sigs chan os.Signal 31 store *store.Store 32 evaler *eval.Evaler 33 cmdSeq int 34 35 ps1 Prompt 36 rps1 Prompt 37 completers map[string]ArgCompleter 38 abbreviations map[string]string 39 40 editorState 41 } 42 43 type editorState struct { 44 // States used during ReadLine. Reset at the beginning of ReadLine. 45 active bool 46 savedTermios *sys.Termios 47 48 notifications []string 49 tips []string 50 51 tokens []Token 52 prompt string 53 rprompt string 54 line string 55 dot int 56 57 mode Mode 58 59 insert insert 60 command command 61 completion completion 62 navigation navigation 63 hist hist 64 histlist *histlist 65 bang *bang 66 location *location 67 68 // A cache of external commands, used in stylist and completer of command 69 // names. 70 isExternal map[string]bool 71 parseErrorAtEnd bool 72 73 // Used for builtins. 74 lastKey Key 75 nextAction action 76 } 77 78 // NewEditor creates an Editor. 79 func NewEditor(file *os.File, sigs chan os.Signal, ev *eval.Evaler, st *store.Store) *Editor { 80 seq := -1 81 if st != nil { 82 var err error 83 seq, err = st.NextCmdSeq() 84 if err != nil { 85 // TODO(xiaq): Also report the error 86 seq = -1 87 } 88 } 89 90 prompt, rprompt := defaultPrompts() 91 92 ed := &Editor{ 93 file: file, 94 writer: newWriter(file), 95 reader: NewReader(file), 96 sigs: sigs, 97 store: st, 98 evaler: ev, 99 cmdSeq: seq, 100 ps1: prompt, 101 rps1: rprompt, 102 103 abbreviations: make(map[string]string), 104 } 105 ev.Modules["le"] = makeModule(ed) 106 return ed 107 } 108 109 func (ed *Editor) flash() { 110 // TODO implement fish-like flash effect 111 } 112 113 func (ed *Editor) addTip(format string, args ...interface{}) { 114 ed.tips = append(ed.tips, fmt.Sprintf(format, args...)) 115 } 116 117 func (ed *Editor) notify(format string, args ...interface{}) { 118 ed.notifications = append(ed.notifications, fmt.Sprintf(format, args...)) 119 } 120 121 func (ed *Editor) refresh(fullRefresh bool, tips bool) error { 122 // Re-lex the line, unless we are in modeCompletion 123 src := ed.line 124 if ed.mode.Mode() != modeCompletion { 125 n, err := parse.Parse(src) 126 ed.parseErrorAtEnd = err != nil && atEnd(err, len(src)) 127 if err != nil { 128 // If all the errors happen at the end, it is liekly complaining 129 // about missing texts that will eventually be inserted. Don't show 130 // such errors. 131 // XXX We may need a more reliable criteria. 132 if tips && !ed.parseErrorAtEnd { 133 ed.addTip("parser error: %s", err) 134 } 135 } 136 if n == nil { 137 ed.tokens = []Token{{ParserError, src, nil, ""}} 138 } else { 139 ed.tokens = tokenize(src, n) 140 _, err := ed.evaler.Compile(n) 141 if err != nil { 142 if tips && !atEnd(err, len(src)) { 143 ed.addTip("compiler error: %s", err) 144 } 145 if err, ok := err.(*util.PosError); ok { 146 p := err.Begin 147 for i, token := range ed.tokens { 148 if token.Node.Begin() <= p && p < token.Node.End() { 149 ed.tokens[i].addStyle(styleForCompilerError) 150 break 151 } 152 } 153 } 154 } 155 } 156 // Apply each stylist on each token. 157 for i, t := range ed.tokens { 158 for _, stylist := range stylists { 159 ed.tokens[i].addStyle(stylist(t.Node, ed)) 160 } 161 } 162 } 163 return ed.writer.refresh(&ed.editorState, fullRefresh) 164 } 165 166 func atEnd(e error, n int) bool { 167 switch e := e.(type) { 168 case *util.PosError: 169 return e.Begin == n 170 case *util.Errors: 171 for _, child := range e.Errors { 172 if !atEnd(child, n) { 173 return false 174 } 175 } 176 return true 177 default: 178 return false 179 } 180 } 181 182 // insertAtDot inserts text at the dot and moves the dot after it. 183 func (ed *Editor) insertAtDot(text string) { 184 ed.line = ed.line[:ed.dot] + text + ed.line[ed.dot:] 185 ed.dot += len(text) 186 } 187 188 func setupTerminal(file *os.File) (*sys.Termios, error) { 189 fd := int(file.Fd()) 190 term, err := sys.NewTermiosFromFd(fd) 191 if err != nil { 192 return nil, fmt.Errorf("can't get terminal attribute: %s", err) 193 } 194 195 savedTermios := term.Copy() 196 197 term.SetICanon(false) 198 term.SetEcho(false) 199 term.SetVMin(1) 200 term.SetVTime(0) 201 202 err = term.ApplyToFd(fd) 203 if err != nil { 204 return nil, fmt.Errorf("can't set up terminal attribute: %s", err) 205 } 206 207 /* 208 err = sys.FlushInput(fd) 209 if err != nil { 210 return nil, fmt.Errorf("can't flush input: %s", err) 211 } 212 */ 213 214 return savedTermios, nil 215 } 216 217 // startReadLine prepares the terminal for the editor. 218 func (ed *Editor) startReadLine() error { 219 savedTermios, err := setupTerminal(ed.file) 220 if err != nil { 221 return err 222 } 223 ed.savedTermios = savedTermios 224 225 _, width := sys.GetWinsize(int(ed.file.Fd())) 226 // Turn on autowrap, write lackEOL along with enough padding to fill the 227 // whole screen. If the cursor was in the first column, we end up in the 228 // same line (just off the line boundary); otherwise we are now in the next 229 // line. We now rewind to the first column and erase anything there. The 230 // final effect is that a lackEOL gets written if and only if the cursor 231 // was not in the first column. 232 fmt.Fprintf(ed.file, "\033[?7h%s%*s\r \r", lackEOL, width-WcWidth(lackEOLRune), "") 233 234 // Turn off autowrap. The edito has its own wrapping mechanism. Doing 235 // wrapping manually means that when the actual width of some characters 236 // are greater than what our wcwidth implementation tells us, characters at 237 // the end of that line gets hidden -- compared to pushed to the next line, 238 // which is more disastrous. 239 ed.file.WriteString("\033[?7l") 240 // Turn on SGR-style mouse tracking. 241 //ed.file.WriteString("\033[?1000;1006h") 242 243 // Enable bracketed paste. 244 ed.file.WriteString("\033[?2004h") 245 246 return nil 247 } 248 249 // finishReadLine puts the terminal in a state suitable for other programs to 250 // use. 251 func (ed *Editor) finishReadLine(addError func(error)) { 252 ed.mode = &ed.insert 253 ed.tips = nil 254 ed.dot = len(ed.line) 255 // TODO Perhaps make it optional to NOT clear the rprompt 256 ed.rprompt = "" 257 addError(ed.refresh(false, false)) 258 ed.file.WriteString("\n") 259 260 // ed.reader.Stop() 261 ed.reader.Quit() 262 263 // Turn on autowrap. 264 ed.file.WriteString("\033[?7h") 265 // Turn off mouse tracking. 266 //ed.file.WriteString("\033[?1000;1006l") 267 268 // Disable bracketed paste. 269 ed.file.WriteString("\033[?2004l") 270 271 // restore termios 272 err := ed.savedTermios.ApplyToFd(int(ed.file.Fd())) 273 274 if err != nil { 275 addError(fmt.Errorf("can't restore terminal attribute: %s", err)) 276 } 277 ed.savedTermios = nil 278 ed.editorState = editorState{} 279 } 280 281 // ReadLine reads a line interactively. 282 func (ed *Editor) ReadLine() (line string, err error) { 283 ed.editorState = editorState{active: true} 284 ed.mode = &ed.insert 285 286 isExternalCh := make(chan map[string]bool, 1) 287 go getIsExternal(ed.evaler, isExternalCh) 288 289 ed.writer.resetOldBuf() 290 go ed.reader.Run() 291 292 e := ed.startReadLine() 293 if e != nil { 294 return "", e 295 } 296 defer ed.finishReadLine(func(e error) { 297 if e != nil { 298 err = util.CatError(err, e) 299 } 300 }) 301 302 fullRefresh := false 303 MainLoop: 304 for { 305 ed.prompt = ed.ps1.Call(ed) 306 ed.rprompt = ed.rps1.Call(ed) 307 308 err := ed.refresh(fullRefresh, true) 309 fullRefresh = false 310 if err != nil { 311 return "", err 312 } 313 314 ed.tips = nil 315 316 select { 317 case m := <-isExternalCh: 318 ed.isExternal = m 319 case sig := <-ed.sigs: 320 // TODO(xiaq): Maybe support customizable handling of signals 321 switch sig { 322 case syscall.SIGINT: 323 // Start over 324 ed.editorState = editorState{ 325 savedTermios: ed.savedTermios, 326 isExternal: ed.isExternal, 327 } 328 ed.mode = &ed.insert 329 goto MainLoop 330 case syscall.SIGWINCH: 331 fullRefresh = true 332 continue MainLoop 333 case syscall.SIGCHLD: 334 // ignore 335 default: 336 ed.addTip("ignored signal %s", sig) 337 } 338 case err := <-ed.reader.ErrorChan(): 339 ed.notify("reader error: %s", err.Error()) 340 case mouse := <-ed.reader.MouseChan(): 341 ed.addTip("mouse: %+v", mouse) 342 case <-ed.reader.CPRChan(): 343 // Ignore CPR 344 case b := <-ed.reader.PasteChan(): 345 if !b { 346 continue 347 } 348 var buf bytes.Buffer 349 timer := time.NewTimer(EscSequenceTimeout) 350 paste: 351 for { 352 // XXX Should also select on other chans. However those chans 353 // will be unified (agina) into one later so we don't do 354 // busywork here. 355 select { 356 case k := <-ed.reader.KeyChan(): 357 if k.Mod != 0 { 358 ed.notify("function key within paste") 359 break paste 360 } 361 buf.WriteRune(k.Rune) 362 timer.Reset(EscSequenceTimeout) 363 case b := <-ed.reader.PasteChan(): 364 if !b { 365 break paste 366 } 367 case <-timer.C: 368 ed.notify("bracketed paste timeout") 369 break paste 370 } 371 } 372 topaste := buf.String() 373 if ed.insert.quotePaste { 374 topaste = parse.Quote(topaste) 375 } 376 ed.insertAtDot(topaste) 377 case k := <-ed.reader.KeyChan(): 378 lookupKey: 379 keyBinding, ok := keyBindings[ed.mode.Mode()] 380 if !ok { 381 ed.addTip("No binding for current mode") 382 continue 383 } 384 385 fn, bound := keyBinding[k] 386 if !bound { 387 fn = keyBinding[Default] 388 } 389 390 ed.insert.insertedLiteral = false 391 ed.lastKey = k 392 fn.Call(ed) 393 if ed.insert.insertedLiteral { 394 ed.insert.literalInserts++ 395 } else { 396 ed.insert.literalInserts = 0 397 } 398 act := ed.nextAction 399 ed.nextAction = action{} 400 401 switch act.typ { 402 case noAction: 403 continue 404 case reprocessKey: 405 err = ed.refresh(false, true) 406 if err != nil { 407 return "", err 408 } 409 goto lookupKey 410 case exitReadLine: 411 if act.returnErr == nil && act.returnLine != "" { 412 ed.appendHistory(act.returnLine) 413 } 414 return act.returnLine, act.returnErr 415 } 416 } 417 } 418 } 419 420 // getIsExternal finds a set of all external commands and puts it on the result 421 // channel. 422 func getIsExternal(ev *eval.Evaler, result chan<- map[string]bool) { 423 names := make(chan string, 32) 424 go func() { 425 ev.AllExecutables(names) 426 close(names) 427 }() 428 isExternal := make(map[string]bool) 429 for name := range names { 430 isExternal[name] = true 431 } 432 result <- isExternal 433 }