github.com/jmigpin/editor@v1.6.0/core/erow.go (about) 1 package core 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "sync" 12 13 "github.com/jmigpin/editor/core/toolbarparser" 14 "github.com/jmigpin/editor/ui" 15 "github.com/jmigpin/editor/util/iout" 16 "github.com/jmigpin/editor/util/iout/iorw" 17 "github.com/jmigpin/editor/util/uiutil/event" 18 ) 19 20 //godebug:annotatefile 21 22 //---------- 23 24 type ERow struct { 25 Ed *Editor 26 Row *ui.Row 27 Info *ERowInfo 28 Exec *ERowExec 29 TbData toolbarparser.Data 30 31 highlightDuplicates bool 32 33 terminalOpt terminalOpt 34 35 ctx context.Context // erow general context 36 cancelCtx context.CancelFunc 37 38 cmd struct { 39 sync.Mutex 40 cancelInternalCmd context.CancelFunc 41 cancelContentCmd context.CancelFunc 42 } 43 } 44 45 //---------- 46 47 func NewLoadedERow(info *ERowInfo, rowPos *ui.RowPos) (*ERow, error) { 48 switch { 49 case info.IsSpecial(): 50 return newLoadedSpecialERow(info, rowPos) 51 case info.IsDir(): 52 return newLoadedDirERow(info, rowPos) 53 case info.IsFileButNotDir(): 54 return newLoadedFileERow(info, rowPos) 55 default: 56 err := fmt.Errorf("unable to open erow: %v", info.name) 57 if info.fiErr != nil { 58 err = fmt.Errorf("%v: %w", err, info.fiErr) 59 } 60 return nil, err 61 } 62 } 63 64 // Allows creating rows in place even if a file/dir doesn't exist anymore (ex: show non-existent files rows in a saved session). 65 func NewLoadedERowOrNewBasic(info *ERowInfo, rowPos *ui.RowPos) *ERow { 66 erow, err := NewLoadedERow(info, rowPos) 67 if err != nil { 68 return NewBasicERow(info, rowPos) 69 } 70 return erow 71 } 72 73 //---------- 74 75 func ExistingERowOrNewLoaded(ed *Editor, name string) (_ *ERow, isNew bool, _ error) { 76 info := ed.ReadERowInfo(name) 77 if erow0, ok := info.FirstERow(); ok { 78 return erow0, false, nil 79 } 80 rowPos := ed.GoodRowPos() 81 erow, err := NewLoadedERow(info, rowPos) 82 if err != nil { 83 return nil, false, err 84 } 85 return erow, true, nil 86 } 87 88 // Used for ex. in: +messages, +sessions. 89 func ExistingERowOrNewBasic(ed *Editor, name string) (_ *ERow, isNew bool) { 90 91 info := ed.ReadERowInfo(name) 92 if erow0, ok := info.FirstERow(); ok { 93 return erow0, false 94 } 95 rowPos := ed.GoodRowPos() 96 erow := NewBasicERow(info, rowPos) 97 return erow, true 98 } 99 100 //---------- 101 102 func NewBasicERow(info *ERowInfo, rowPos *ui.RowPos) *ERow { 103 erow := &ERow{} 104 erow.init(info, rowPos) 105 return erow 106 } 107 108 func (erow *ERow) init(info *ERowInfo, rowPos *ui.RowPos) { 109 erow.Ed = info.Ed 110 erow.Info = info 111 erow.Row = rowPos.Column.NewRowBefore(rowPos.NextRow) 112 erow.Exec = NewERowExec(erow) 113 114 ctx0 := context.Background() // TODO: editor ctx 115 erow.ctx, erow.cancelCtx = context.WithCancel(ctx0) 116 117 erow.setupSyntaxHighlightAndCommentShortcuts() 118 erow.initHandlers() 119 120 erow.updateToolbarNameEncoding2("") 121 122 // editor events 123 ev := &PostNewERowEEvent{ERow: erow} 124 erow.Ed.EEvents.emit(PostNewERowEEventId, ev) 125 } 126 127 //---------- 128 129 func newLoadedSpecialERow(info *ERowInfo, rowPos *ui.RowPos) (*ERow, error) { 130 // there can be only one instance of a special row 131 if len(info.ERows) > 0 { 132 return nil, fmt.Errorf("special row already exists: %v", info.Name()) 133 134 } 135 erow := NewBasicERow(info, rowPos) 136 // load 137 switch { 138 case info.Name() == "+Sessions": 139 ListSessions(erow.Ed) 140 } 141 return erow, nil 142 } 143 144 func newLoadedDirERow(info *ERowInfo, rowPos *ui.RowPos) (*ERow, error) { 145 if !info.IsDir() { 146 return nil, fmt.Errorf("not a directory") 147 } 148 erow := NewBasicERow(info, rowPos) 149 // load 150 ListDirERow(erow, erow.Info.Name(), false, true) 151 return erow, nil 152 } 153 154 func newLoadedFileERow(info *ERowInfo, rowPos *ui.RowPos) (*ERow, error) { 155 // read content from existing row 156 if erow0, ok := info.FirstERow(); ok { 157 // create erow first to get it updated 158 erow := NewBasicERow(info, rowPos) 159 // update the new erow with content 160 info.setRWFromMaster(erow0) 161 return erow, nil 162 } 163 164 // load 165 b, err := info.readFsFile() 166 if err != nil { 167 return nil, err 168 } 169 170 // update data 171 info.setSavedHash(info.fileData.fs.hash, len(b)) 172 173 // new erow (no other rows exist) 174 erow := NewBasicERow(info, rowPos) 175 erow.Row.TextArea.SetBytesClearHistory(b) 176 177 return erow, nil 178 } 179 180 //---------- 181 182 func (erow *ERow) Reload() { 183 if err := erow.reload(); err != nil { 184 erow.Ed.Error(err) 185 } 186 } 187 188 func (erow *ERow) reload() error { 189 switch { 190 case erow.Info.IsSpecial() && erow.Info.Name() == "+Sessions": 191 ListSessions(erow.Ed) 192 return nil 193 case erow.Info.IsDir(): 194 ListDirERow(erow, erow.Info.Name(), false, true) 195 return nil 196 case erow.Info.IsFileButNotDir(): 197 return erow.Info.ReloadFile() 198 default: 199 return errors.New("unexpected type to reload") 200 } 201 } 202 203 //---------- 204 205 func (erow *ERow) initHandlers() { 206 row := erow.Row 207 208 // register with the editor 209 erow.Ed.SetERowInfo(erow.Info.Name(), erow.Info) 210 erow.Info.AddERow(erow) 211 212 // update row state 213 erow.Info.UpdateDuplicateRowState() 214 erow.Info.UpdateDuplicateHighlightRowState() 215 erow.Info.UpdateExistsRowState() 216 erow.Info.UpdateFsDifferRowState() 217 218 // register with watcher 219 if !erow.Info.IsSpecial() && len(erow.Info.ERows) == 1 { 220 erow.Ed.Watcher.Add(erow.Info.Name()) 221 } 222 223 // toolbar on prewrite 224 row.Toolbar.RWEvReg.Add(iorw.RWEvIdPreWrite, func(ev0 interface{}) { 225 ev := ev0.(*iorw.RWEvPreWrite) 226 if err := erow.validateToolbarPreWrite(ev); err != nil { 227 ev.ReplyErr = err 228 } 229 }) 230 // toolbar cmds 231 row.Toolbar.EvReg.Add(ui.TextAreaCmdEventId, func(ev0 interface{}) { 232 InternalCmdFromRowTb(erow) 233 }) 234 // textarea on write 235 row.TextArea.RWEvReg.Add(iorw.RWEvIdWrite2, func(ev0 interface{}) { 236 ev := ev0.(*iorw.RWEvWrite2) 237 erow.Info.HandleRWEvWrite2(erow, ev) 238 }) 239 // textarea content cmds 240 row.TextArea.EvReg.Add(ui.TextAreaCmdEventId, func(ev0 interface{}) { 241 ev := ev0.(*ui.TextAreaCmdEvent) 242 ContentCmdFromTextArea(erow, ev.Index) 243 }) 244 // textarea select annotation 245 row.TextArea.EvReg.Add(ui.TextAreaSelectAnnotationEventId, func(ev interface{}) { 246 ev2 := ev.(*ui.TextAreaSelectAnnotationEvent) 247 erow.Ed.GoDebug.SelectERowAnnotation(erow, ev2) 248 }) 249 // textarea inlinecomplete 250 row.TextArea.EvReg.Add(ui.TextAreaInlineCompleteEventId, func(ev0 interface{}) { 251 ev := ev0.(*ui.TextAreaInlineCompleteEvent) 252 handled := erow.Ed.InlineComplete.Complete(erow, ev) 253 // Allow the input event (`tab` key press) to function normally if the inlinecomplete is not being handled (ex: no lsproto server is registered for this filename extension) 254 ev.ReplyHandled = event.Handled(handled) 255 }) 256 // key shortcuts 257 row.EvReg.Add(ui.RowInputEventId, func(ev0 interface{}) { 258 erow.Ed.InlineComplete.CancelOnCursorChange() 259 260 ev := ev0.(*ui.RowInputEvent) 261 switch evt := ev.Event.(type) { 262 case *event.KeyDown: 263 // activate row 264 erow.Info.UpdateActiveRowState(erow) 265 // shortcuts 266 mods := evt.Mods.ClearLocks() 267 switch { 268 case mods.Is(event.ModCtrl) && evt.KeySym == event.KSymS: 269 if err := erow.Info.SaveFile(); err != nil { 270 erow.Ed.Error(err) 271 } 272 case mods.Is(event.ModCtrl) && evt.KeySym == event.KSymF: 273 FindShortcut(erow) 274 case mods.Is(event.ModCtrl) && evt.KeySym == event.KSymH: 275 ReplaceShortcut(erow) 276 case mods.Is(event.ModCtrl) && evt.KeySym == event.KSymN: 277 NewFileShortcut(erow) 278 case mods.Is(event.ModCtrl) && evt.KeySym == event.KSymW: 279 row.Close() 280 case evt.KeySym == event.KSymEscape: 281 erow.Exec.Stop() 282 } 283 case *event.MouseDown: 284 erow.Info.UpdateActiveRowState(erow) 285 case *event.MouseEnter: 286 erow.highlightDuplicates = true 287 erow.Info.UpdateDuplicateHighlightRowState() 288 case *event.MouseLeave: 289 erow.highlightDuplicates = false 290 erow.Info.UpdateDuplicateHighlightRowState() 291 } 292 }) 293 // close 294 row.EvReg.Add(ui.RowCloseEventId, func(ev0 interface{}) { 295 // editor events 296 ev := &PreRowCloseEEvent{ERow: erow} 297 erow.Ed.EEvents.emit(PreRowCloseEEventId, ev) 298 299 // cancel general context 300 erow.cancelCtx() 301 302 // ensure execution (if any) is stopped 303 erow.Exec.Stop() 304 305 // unregister from editor 306 erow.Info.RemoveERow(erow) 307 if len(erow.Info.ERows) == 0 { 308 erow.Ed.DeleteERowInfo(erow.Info.Name()) 309 } 310 311 // update row state 312 erow.Info.UpdateDuplicateRowState() 313 erow.Info.UpdateDuplicateHighlightRowState() 314 315 // unregister with watcher 316 if !erow.Info.IsSpecial() && len(erow.Info.ERows) == 0 { 317 erow.Ed.Watcher.Remove(erow.Info.Name()) 318 } 319 320 // add to reopener to allow to reopen later if needed 321 if !erow.Info.IsSpecial() { 322 erow.Ed.RowReopener.Add(row) 323 } 324 }) 325 } 326 327 //---------- 328 329 func (erow *ERow) encodedName() string { 330 return erow.Ed.HomeVars.Encode(erow.Info.Name()) 331 } 332 333 //---------- 334 335 func (erow *ERow) validateToolbarPreWrite(ev *iorw.RWEvPreWrite) error { 336 // current content (pre write) copy 337 b, err := iorw.ReadFullCopy(erow.Row.Toolbar.RW()) 338 if err != nil { 339 return err 340 } 341 342 // simulate the write 343 // TODO: how to guarantee the simulation is accurate and no rw filter exists. 344 rw := iorw.NewBytesReadWriterAt(b) 345 if err := rw.OverwriteAt(ev.Index, ev.N, ev.P); err != nil { 346 return err 347 } 348 b2, err := iorw.ReadFastFull(rw) 349 if err != nil { 350 return err 351 } 352 tbStr2 := string(b2) 353 354 // simulation name 355 data := toolbarparser.Parse(tbStr2) 356 arg0, ok := data.Part0Arg0() 357 if !ok { 358 return fmt.Errorf("unable to get toolbar name") 359 } 360 simName := arg0.UnquotedString() 361 362 // expected name 363 nameEnc := erow.encodedName() 364 365 if simName != nameEnc { 366 return fmt.Errorf("can't change toolbar name: %q -> %q", nameEnc, simName) 367 } 368 369 // valid data 370 erow.TbData = *data 371 erow.parseToolbarVars() 372 373 return nil 374 } 375 376 //---------- 377 378 func (erow *ERow) UpdateToolbarNameEncoding() { 379 str := erow.Row.Toolbar.Str() 380 erow.updateToolbarNameEncoding2(str) 381 } 382 383 func (erow *ERow) updateToolbarNameEncoding2(str string) { 384 arg0End := 0 385 data := toolbarparser.Parse(str) 386 arg0, ok := data.Part0Arg0() 387 if ok { 388 arg0End = arg0.End() 389 } 390 391 // replace part0 arg0 with encoded name 392 ename := erow.encodedName() 393 str2 := ename + str[arg0End:] 394 if str2 != str { 395 erow.Row.Toolbar.SetStrClearHistory(str2) 396 } 397 } 398 399 func (erow *ERow) ToolbarSetStrAfterNameClearHistory(s string) { 400 arg0, ok := erow.TbData.Part0Arg0() 401 if !ok { 402 return 403 } 404 str := erow.Row.Toolbar.Str()[:arg0.End()] + s 405 erow.Row.Toolbar.SetStrClearHistory(str) 406 } 407 408 // ---------- 409 func (erow *ERow) parseToolbarVars() { 410 vmap := toolbarparser.ParseVars(&erow.TbData) 411 412 // $font 413 clear := true 414 if v, ok := vmap["$font"]; ok { 415 err := erow.setVarFontTheme(v) 416 if err == nil { 417 clear = false 418 } 419 } 420 if clear { 421 erow.Row.TextArea.SetThemeFontFace(nil) 422 } 423 424 // $terminal 425 erow.terminalOpt = terminalOpt{} 426 if erow.Info.IsDir() { 427 // Deprecated: use $terminal 428 if _, ok := vmap["$termFilter"]; ok { 429 erow.terminalOpt.filter = true 430 } 431 432 if v, ok := vmap["$terminal"]; ok { 433 u := strings.Split(v, ",") 434 for _, k := range u { 435 switch k { 436 case "f": 437 erow.terminalOpt.filter = true 438 case "k": 439 erow.terminalOpt.keyEvents = true 440 } 441 } 442 } 443 } 444 } 445 446 func (erow *ERow) setVarFontTheme(s string) error { 447 w := strings.SplitN(s, ",", 2) 448 name := w[0] 449 450 // font size arg 451 size := float64(0) 452 if len(w) > 1 { 453 v, err := strconv.ParseFloat(w[1], 64) 454 if err != nil { 455 // commented: ignore error 456 //return err 457 } else { 458 size = v 459 } 460 } 461 462 ff, err := ui.ThemeFontFace2(name, size) 463 if err != nil { 464 return err 465 } 466 erow.Row.TextArea.SetThemeFontFace(ff) 467 return nil 468 } 469 470 //---------- 471 472 // Not UI safe. 473 func (erow *ERow) TextAreaAppendBytes(p []byte) { 474 ta := erow.Row.TextArea 475 if err := ta.AppendBytesClearHistory(p); err != nil { 476 erow.Ed.Error(err) 477 } 478 } 479 480 // UI safe, with option to wait. 481 func (erow *ERow) TextAreaAppendBytesAsync(p []byte) func() { 482 var wg sync.WaitGroup 483 wg.Add(1) 484 erow.Ed.UI.RunOnUIGoRoutine(func() { 485 erow.TextAreaAppendBytes(p) 486 wg.Done() 487 }) 488 return wg.Wait 489 } 490 491 //---------- 492 493 func (erow *ERow) TextAreaReadWriteCloser() io.ReadWriteCloser { 494 if erow.terminalOpt.On() { 495 return NewTerminalFilter(erow) 496 } 497 498 // synced writer to slow down memory usage 499 w := iout.FnWriter(func(b []byte) (int, error) { 500 var err error 501 erow.Ed.UI.WaitRunOnUIGoRoutine(func() { 502 err = erow.Row.TextArea.AppendBytesClearHistory(b) 503 }) 504 return len(b), err 505 }) 506 507 // buffered for performance, which needs timed output (auto-flush) 508 wc := iout.NewAutoBufWriter(w, 4096*2) 509 510 rd := iout.FnReader(func(b []byte) (int, error) { return 0, io.EOF }) 511 type iorwc struct { 512 io.Reader 513 io.Writer 514 io.Closer 515 } 516 return io.ReadWriteCloser(&iorwc{rd, wc, wc}) 517 } 518 519 //---------- 520 521 // UI Safe 522 func (erow *ERow) Flash() { 523 p, ok := erow.TbData.PartAtIndex(0) 524 if ok { 525 if len(p.Args) > 0 { 526 a := p.Args[0] 527 erow.Row.Toolbar.FlashIndexLen(a.Pos(), a.End()-a.Pos()) 528 } 529 } 530 } 531 532 //---------- 533 534 func (erow *ERow) MakeIndexVisibleAndFlash(index int) { 535 erow.MakeRangeVisibleAndFlash(index, 0) 536 } 537 538 func (erow *ERow) MakeRangeVisibleAndFlash(index int, len int) { 539 // Commented: don't flicker row positions 540 //erow.Row.EnsureTextAreaMinimumHeight() 541 542 erow.Row.EnsureOneToolbarLineYVisible() 543 544 erow.Row.TextArea.MakeRangeVisible(index, len) 545 erow.Row.TextArea.FlashIndexLen(index, len) 546 547 // flash toolbar as last resort if less visible 548 ta := erow.Row.TextArea 549 lh := ta.LineHeight() 550 min := int(float64(lh) * 1.5) 551 if ta.Bounds.Dy() < min { 552 erow.Flash() 553 } 554 } 555 556 //---------- 557 558 func (erow *ERow) setupSyntaxHighlightAndCommentShortcuts() { 559 // special handling for the toolbar (allow comment shortcut to work in the toolbar to easily disable cmds) 560 tb := erow.Row.Toolbar 561 tb.SetCommentStrings("#") 562 563 ta := erow.Row.TextArea 564 565 // ensure syntax highlight is on (ex: strings) 566 ta.EnableSyntaxHighlight(true) 567 568 //// consider only files from here (dirs and special rows are out) 569 //if !erow.Info.IsFileButNotDir() { 570 // return 571 //} 572 573 // util funcs 574 setComments := func(a ...interface{}) { 575 ta.SetCommentStrings(a...) 576 } 577 578 // ignore "." on files starting with "." 579 name := filepath.Base(erow.Info.Name()) 580 if len(name) >= 1 && name[0] == '.' { 581 name = name[1:] 582 } 583 584 // specific names 585 switch name { 586 case "bashrc": 587 setComments("#") 588 return 589 case "go.mod": 590 setComments("//") 591 return 592 } 593 594 // setup comments based on name extension 595 ext := strings.ToLower(filepath.Ext(name)) 596 switch ext { 597 case ".sh", 598 ".conf", ".list", 599 ".py", // python 600 ".pl": // perl 601 setComments("#") 602 case ".go", 603 ".c", ".h", 604 ".cpp", ".hpp", ".cxx", ".hxx", // c++ 605 ".java", 606 ".v", // verilog 607 ".js": // javascript 608 setComments("//", [2]string{"/*", "*/"}) 609 case ".ledger": 610 setComments(";", "//") 611 case ".pro": // prolog 612 setComments("%", [2]string{"/*", "*/"}) 613 case ".html", ".xml", ".svg": 614 setComments([2]string{"<!--", "-->"}) 615 case ".s", ".asm": // assembly 616 setComments("//") 617 case ".json": 618 // no comments to setup 619 case ".txt": 620 setComments("#") // useful (but not correct) 621 case "": // no file extension 622 // handy for ex: /etc/network/interfaces 623 setComments("#") // useful (but not correct) 624 default: // all other file extensions 625 setComments("#") // useful (but not correct) 626 } 627 } 628 629 //---------- 630 631 func (erow *ERow) newContentCmdCtx() (context.Context, context.CancelFunc) { 632 erow.cmd.Lock() 633 defer erow.cmd.Unlock() 634 erow.cancelContentCmd2() 635 ctx, cancel := context.WithCancel(erow.ctx) 636 erow.cmd.cancelContentCmd = cancel 637 return ctx, cancel 638 } 639 func (erow *ERow) CancelContentCmd() { 640 erow.cmd.Lock() 641 defer erow.cmd.Unlock() 642 erow.cancelContentCmd2() 643 } 644 func (erow *ERow) cancelContentCmd2() { 645 if erow.cmd.cancelContentCmd != nil { 646 erow.cmd.cancelContentCmd() 647 } 648 } 649 650 //---------- 651 652 func (erow *ERow) newInternalCmdCtx() (context.Context, context.CancelFunc) { 653 erow.cmd.Lock() 654 defer erow.cmd.Unlock() 655 erow.cancelInternalCmd2() 656 ctx, cancel := context.WithCancel(erow.ctx) 657 erow.cmd.cancelInternalCmd = cancel 658 return ctx, cancel 659 } 660 661 func (erow *ERow) CancelInternalCmd() { 662 erow.cmd.Lock() 663 defer erow.cmd.Unlock() 664 erow.cancelInternalCmd2() 665 } 666 func (erow *ERow) cancelInternalCmd2() { 667 if erow.cmd.cancelInternalCmd != nil { 668 erow.cmd.cancelInternalCmd() 669 } 670 } 671 672 //---------- 673 674 type terminalOpt struct { 675 filter bool 676 keyEvents bool 677 } 678 679 func (t *terminalOpt) On() bool { 680 return t.filter || t.keyEvents 681 }