github.com/jmigpin/editor@v1.6.0/core/editor.go (about) 1 package core 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "log" 8 "os" 9 "path/filepath" 10 "runtime" 11 "sort" 12 "strings" 13 "time" 14 "unicode" 15 16 "github.com/jmigpin/editor/core/fswatcher" 17 "github.com/jmigpin/editor/core/lsproto" 18 "github.com/jmigpin/editor/ui" 19 "github.com/jmigpin/editor/util/drawutil/drawer4" 20 "github.com/jmigpin/editor/util/fontutil" 21 "github.com/jmigpin/editor/util/imageutil" 22 "github.com/jmigpin/editor/util/iout/iorw" 23 "github.com/jmigpin/editor/util/osutil" 24 "github.com/jmigpin/editor/util/uiutil/event" 25 "github.com/jmigpin/editor/util/uiutil/widget" 26 "golang.org/x/image/font" 27 ) 28 29 type Editor struct { 30 UI *ui.UI 31 HomeVars *HomeVars 32 Watcher fswatcher.Watcher 33 RowReopener *RowReopener 34 GoDebug *GoDebugManager 35 LSProtoMan *lsproto.Manager 36 InlineComplete *InlineComplete 37 Plugins *Plugins 38 EEvents *EEvents // editor events (used by plugins) 39 FsCaseInsensitive bool // filesystem 40 41 dndh *DndHandler 42 ifbw *InfoFloatBoxWrap 43 erowInfos map[string]*ERowInfo // use ed.ERowInfo*() to access 44 preSaveHooks []*PreSaveHook 45 } 46 47 func RunEditor(opt *Options) error { 48 ed := &Editor{} 49 ed.erowInfos = map[string]*ERowInfo{} 50 ed.ifbw = NewInfoFloatBox(ed) 51 52 // TODO: osx can have a case insensitive filesystem 53 ed.FsCaseInsensitive = runtime.GOOS == "windows" 54 55 ed.HomeVars = NewHomeVars() 56 ed.RowReopener = NewRowReopener(ed) 57 ed.dndh = NewDndHandler(ed) 58 ed.GoDebug = NewGoDebugManager(ed) 59 ed.InlineComplete = NewInlineComplete(ed) 60 ed.EEvents = NewEEvents() 61 62 if err := ed.init(opt); err != nil { 63 return err 64 } 65 66 go ed.fswatcherEventLoop() 67 ed.uiEventLoop() // blocks 68 69 return nil 70 } 71 72 //---------- 73 74 func (ed *Editor) init(opt *Options) error { 75 // fs watcher + gwatcher 76 w, err := fswatcher.NewFsnWatcher() 77 if err != nil { 78 return err 79 } 80 ed.Watcher = fswatcher.NewGWatcher(w) 81 82 ed.setupTheme(opt) 83 event.UseMultiKey = opt.UseMultiKey 84 85 // user interface 86 ui0, err := ui.NewUI("Editor") 87 if err != nil { 88 return err 89 } 90 ed.UI = ui0 91 ed.UI.OnError = ed.Error 92 ed.setupUIRoot() 93 94 // TODO: ensure it has the window measure 95 ed.EnsureOneColumn() 96 97 // setup plugins 98 setupInitialRows := true 99 err = ed.setupPlugins(opt) 100 if err != nil { 101 ed.Error(err) 102 setupInitialRows = false 103 } 104 105 if setupInitialRows { 106 // enqueue setup initial rows to run after UI has window measure 107 ed.UI.RunOnUIGoRoutine(func() { 108 ed.setupInitialRows(opt) 109 }) 110 } 111 112 ed.initLSProto(opt) 113 ed.initPreSaveHooks(opt) 114 115 return nil 116 } 117 118 func (ed *Editor) initLSProto(opt *Options) { 119 // language server protocol manager 120 ed.LSProtoMan = lsproto.NewManager(ed.Message) 121 for _, reg := range opt.LSProtos.regs { 122 ed.LSProtoMan.Register(reg) 123 } 124 125 // NOTE: argument for not having auto-registration: don't auto add since the lsproto server could have issues, and auto-adding doesn't allow the user to have a choice to using directly some other option (like a plugin) 126 // NOTE: unlikely to be using a plugin for golang since gopls is fairly stable now, allow auto registration at least for ".go" if not present 127 128 // auto setup gopls if there is no handler for ".go" files 129 _, err := ed.LSProtoMan.LangManager("a.go") 130 if err != nil { // no registration exists 131 s := lsproto.GoplsRegistration(false, false, false) 132 reg, err := lsproto.NewRegistration(s) 133 if err != nil { 134 panic(err) 135 } 136 _ = ed.LSProtoMan.Register(reg) 137 } 138 } 139 140 func (ed *Editor) initPreSaveHooks(opt *Options) { 141 // auto register "goimports" if no entry exists for the "go" language 142 found := false 143 for _, r := range opt.PreSaveHooks.regs { 144 if r.Language == "go" { 145 found = true 146 break 147 } 148 } 149 if !found { 150 exec := osutil.ExecName("goimports") 151 opt.PreSaveHooks.MustSet("go,.go," + exec) 152 } 153 154 ed.preSaveHooks = opt.PreSaveHooks.regs 155 } 156 157 //---------- 158 159 func (ed *Editor) Close() { 160 ed.LSProtoMan.Close() 161 ed.UI.AppendEvent(&editorCloseEv{}) 162 } 163 164 //---------- 165 166 func (ed *Editor) uiEventLoop() { 167 defer ed.UI.Close() 168 169 for { 170 ev := ed.UI.NextEvent() 171 switch t := ev.(type) { 172 case error: 173 log.Println(t) // in case there is no window yet (TODO: detect?) 174 ed.Error(t) 175 case *editorCloseEv: 176 return 177 case *event.WindowClose: 178 return 179 case *event.DndPosition: 180 ed.dndh.OnPosition(t) 181 case *event.DndDrop: 182 ed.dndh.OnDrop(t) 183 default: 184 //if !ed.handleGlobalShortcuts(ev) { 185 // if !ed.UI.HandleEvent(ev) { 186 // log.Printf("uievloop: unhandled event: %#v", ev) 187 // } 188 //} 189 h1 := ed.handleGlobalShortcuts(ev) 190 h2 := ed.UI.HandleEvent(ev) 191 if !h1 && !h2 { 192 log.Printf("uievloop: unhandled event: %#v", ev) 193 } 194 } 195 ed.UI.LayoutMarkedAndSchedulePaint() 196 } 197 } 198 199 //---------- 200 201 func (ed *Editor) fswatcherEventLoop() { 202 for { 203 select { 204 case ev, ok := <-ed.Watcher.Events(): 205 if !ok { 206 ed.Close() 207 return 208 } 209 switch evt := ev.(type) { 210 case error: 211 ed.Error(evt) 212 case *fswatcher.Event: 213 ed.handleWatcherEvent(evt) 214 } 215 } 216 } 217 } 218 219 func (ed *Editor) handleWatcherEvent(ev *fswatcher.Event) { 220 info, ok := ed.ERowInfo(ev.Name) 221 if ok { 222 ed.UI.RunOnUIGoRoutine(func() { 223 info.UpdateDiskEvent() 224 }) 225 } 226 } 227 228 //---------- 229 230 func (ed *Editor) Errorf(f string, a ...interface{}) { 231 ed.Error(fmt.Errorf(f, a...)) 232 } 233 func (ed *Editor) Error(err error) { 234 ed.Messagef("error: %v", err) 235 } 236 237 func (ed *Editor) Messagef(f string, a ...interface{}) { 238 ed.Message(fmt.Sprintf(f, a...)) 239 } 240 241 func (ed *Editor) Message(s string) { 242 // ensure newline 243 if !strings.HasSuffix(s, "\n") { 244 s = s + "\n" 245 } 246 247 ed.UI.RunOnUIGoRoutine(func() { 248 erow := ed.messagesERow() 249 250 // index to make visible, get before append 251 ta := erow.Row.TextArea 252 index := ta.Len() 253 254 erow.TextAreaAppendBytes([]byte(s)) 255 256 // don't count spaces at the end for closer bottom alignment 257 u := strings.TrimRightFunc(s, unicode.IsSpace) 258 erow.MakeRangeVisibleAndFlash(index, len(u)) 259 }) 260 } 261 262 //---------- 263 264 func (ed *Editor) messagesERow() *ERow { 265 erow, isNew := ExistingERowOrNewBasic(ed, "+Messages") 266 if isNew { 267 erow.ToolbarSetStrAfterNameClearHistory(" | Clear") 268 } 269 return erow 270 } 271 272 //---------- 273 274 func (ed *Editor) ReadERowInfo(name string) *ERowInfo { 275 return readERowInfoOrNew(ed, name) 276 } 277 278 func (ed *Editor) ERowInfo(name string) (*ERowInfo, bool) { 279 k := ed.ERowInfoKey(name) 280 info, ok := ed.erowInfos[k] 281 return info, ok 282 } 283 284 func (ed *Editor) ERowInfos() []*ERowInfo { 285 // stable list 286 keys := []string{} 287 for k := range ed.erowInfos { 288 keys = append(keys, k) 289 } 290 sort.Strings(keys) 291 292 u := make([]*ERowInfo, len(ed.erowInfos)) 293 for i, k := range keys { 294 u[i] = ed.erowInfos[k] 295 } 296 return u 297 } 298 299 func (ed *Editor) ERowInfoKey(name string) string { 300 if ed.FsCaseInsensitive { 301 return strings.ToLower(name) 302 } 303 return name 304 } 305 306 func (ed *Editor) SetERowInfo(name string, info *ERowInfo) { 307 k := ed.ERowInfoKey(name) 308 ed.erowInfos[k] = info 309 } 310 311 func (ed *Editor) DeleteERowInfo(name string) { 312 k := ed.ERowInfoKey(name) 313 delete(ed.erowInfos, k) 314 } 315 316 //---------- 317 318 func (ed *Editor) ERows() []*ERow { 319 w := []*ERow{} 320 for _, info := range ed.ERowInfos() { 321 for _, e := range info.ERows { 322 w = append(w, e) 323 } 324 } 325 return w 326 } 327 328 //---------- 329 330 func (ed *Editor) GoodRowPos() *ui.RowPos { 331 return ed.UI.GoodRowPos() 332 } 333 334 //---------- 335 336 func (ed *Editor) ActiveERow() (*ERow, bool) { 337 for _, e := range ed.ERows() { 338 if e.Row.HasState(ui.RowStateActive) { 339 return e, true 340 } 341 } 342 return nil, false 343 } 344 345 //---------- 346 347 func (ed *Editor) setupUIRoot() { 348 ed.setupRootToolbar() 349 ed.setupRootMenuToolbar() 350 351 // ui.root select annotation 352 ed.UI.Root.EvReg.Add(ui.RootSelectAnnotationEventId, func(ev interface{}) { 353 rowPos := ed.GoodRowPos() 354 ev2 := ev.(*ui.RootSelectAnnotationEvent) 355 ed.GoDebug.SelectAnnotation(rowPos, ev2) 356 }) 357 } 358 359 func (ed *Editor) setupRootToolbar() { 360 tb := ed.UI.Root.Toolbar 361 // cmd event 362 tb.EvReg.Add(ui.TextAreaCmdEventId, func(ev interface{}) { 363 InternalCmdFromRootTb(ed, tb) 364 }) 365 // on write 366 tb.RWEvReg.Add(iorw.RWEvIdWrite, func(ev0 interface{}) { 367 ed.updateERowsToolbarsHomeVars() 368 }) 369 370 s := "Exit | ListSessions | NewColumn | NewRow | Reload | Stop" 371 tb.SetStrClearHistory(s) 372 } 373 374 func (ed *Editor) setupRootMenuToolbar() { 375 tb := ed.UI.Root.MainMenuButton.Toolbar 376 // cmd event 377 tb.EvReg.Add(ui.TextAreaCmdEventId, func(ev interface{}) { 378 InternalCmdFromRootTb(ed, tb) 379 }) 380 // on write 381 tb.RWEvReg.Add(iorw.RWEvIdWrite, func(ev0 interface{}) { 382 ed.updateERowsToolbarsHomeVars() 383 }) 384 385 w := [][]string{ 386 []string{"ColorTheme", "FontTheme"}, 387 []string{"FontRunes", "RuneCodes"}, 388 []string{"NewFile", "SaveAllFiles", "Save"}, 389 []string{"Reload", "ReloadAll", "ReloadAllFiles"}, 390 []string{"NewColumn", "NewRow", "ReopenRow", "MaximizeRow"}, 391 []string{"ListDir", "ListDir -hidden", "ListDir -sub"}, 392 []string{"ListSessions", "OpenSession", "DeleteSession", "SaveSession"}, 393 []string{"GoDebug -h", "GoRename"}, 394 []string{"LsprotoRename", "LsprotoCloseAll", "LsprotoCallers", "LsprotoCallees", "LsprotoReferences"}, 395 []string{"OpenFilemanager", "OpenTerminal"}, 396 397 []string{"GotoLine"}, 398 []string{"CopyFilePosition"}, 399 []string{"CtxutilCallsState"}, 400 []string{"Find -h"}, 401 } 402 last := []string{"Exit", "Version", "Stop", "Clear"} 403 404 // simple sorted list 405 w2 := []string{} 406 for _, a := range w { 407 w2 = append(w2, a...) 408 } 409 sort.Slice(w2, func(a, b int) bool { return w2[a] < w2[b] }) 410 w2 = append(w2, "\n"+strings.Join(last, " | ")) 411 s1 := strings.Join(w2, "\n") 412 413 tb.SetStrClearHistory(s1) 414 } 415 416 //---------- 417 418 func (ed *Editor) updateERowsToolbarsHomeVars() { 419 tb1 := ed.UI.Root.Toolbar.Str() 420 tb2 := ed.UI.Root.MainMenuButton.Toolbar.Str() 421 ed.HomeVars.ParseToolbarVars([]string{tb1, tb2}, ed.FsCaseInsensitive) 422 for _, erow := range ed.ERows() { 423 erow.UpdateToolbarNameEncoding() 424 } 425 } 426 427 //---------- 428 429 func (ed *Editor) setupInitialRows(opt *Options) { 430 if opt.SessionName != "" { 431 OpenSessionFromString(ed, opt.SessionName) 432 return 433 } 434 435 // cmd line filenames to open 436 if len(opt.Filenames) > 0 { 437 col := ed.UI.Root.Cols.FirstChildColumn() 438 for _, filename := range opt.Filenames { 439 // try to use absolute path 440 u, err := filepath.Abs(filename) 441 if err == nil { 442 filename = u 443 } 444 445 info := ed.ReadERowInfo(filename) 446 if len(info.ERows) == 0 { 447 rowPos := ui.NewRowPos(col, nil) 448 _, err := NewLoadedERow(info, rowPos) 449 if err != nil { 450 ed.Error(err) 451 } 452 } 453 } 454 return 455 } 456 457 // open current directory 458 dir, err := os.Getwd() 459 if err == nil { 460 // create a second column (one should exist already) 461 _ = ed.NewColumn() 462 463 // open directory 464 info := ed.ReadERowInfo(dir) 465 cols := ed.UI.Root.Cols 466 rowPos := ui.NewRowPos(cols.LastChildColumn(), nil) 467 _ = NewLoadedERowOrNewBasic(info, rowPos) 468 } 469 } 470 471 //---------- 472 473 func (ed *Editor) setupTheme(opt *Options) { 474 drawer4.WrapLineRune = rune(opt.WrapLineRune) 475 fontutil.TabWidth = opt.TabWidth 476 ui.ScrollBarLeft = opt.ScrollBarLeft 477 ui.ScrollBarWidth = opt.ScrollBarWidth 478 ui.ShadowsOn = opt.Shadows 479 480 // color theme 481 if _, ok := ui.ColorThemeCycler.GetIndex(opt.ColorTheme); !ok { 482 fmt.Fprintf(os.Stderr, "unknown color theme: %v\n", opt.ColorTheme) 483 os.Exit(2) 484 } 485 ui.ColorThemeCycler.CurName = opt.ColorTheme 486 487 // color comments 488 if opt.CommentsColor != 0 { 489 ui.TextAreaCommentsColor = imageutil.RgbaFromInt(opt.CommentsColor) 490 } 491 492 // color strings 493 if opt.StringsColor != 0 { 494 ui.TextAreaStringsColor = imageutil.RgbaFromInt(opt.StringsColor) 495 } 496 497 // font options 498 fontutil.DPI = opt.DPI 499 ui.TTFontOptions.DPI = opt.DPI 500 ui.TTFontOptions.Size = opt.FontSize 501 switch opt.FontHinting { 502 case "none": 503 ui.TTFontOptions.Hinting = font.HintingNone 504 case "vertical": 505 ui.TTFontOptions.Hinting = font.HintingVertical 506 case "full": 507 ui.TTFontOptions.Hinting = font.HintingFull 508 default: 509 fmt.Fprintf(os.Stderr, "unknown font hinting: %v\n", opt.FontHinting) 510 os.Exit(2) 511 } 512 513 // font theme 514 if _, ok := ui.FontThemeCycler.GetIndex(opt.Font); ok { 515 ui.FontThemeCycler.CurName = opt.Font 516 } else { 517 // font filename 518 err := ui.AddUserFont(opt.Font) 519 if err != nil { 520 // can't send error to UI since it's not created yet 521 log.Print(err) 522 523 // could fail and abort, but instead continue with a known font 524 ui.FontThemeCycler.CurName = "regular" 525 } 526 } 527 } 528 529 //---------- 530 531 func (ed *Editor) setupPlugins(opt *Options) error { 532 ed.Plugins = NewPlugins(ed) 533 a := strings.Split(opt.Plugins, ",") 534 for _, s := range a { 535 s = strings.TrimSpace(s) 536 if s == "" { 537 continue 538 } 539 err := ed.Plugins.AddPath(s) 540 if err != nil { 541 return err 542 } 543 } 544 return nil 545 } 546 547 //---------- 548 549 func (ed *Editor) EnsureOneColumn() { 550 if ed.UI.Root.Cols.ColsLayout.Spl.ChildsLen() == 0 { 551 _ = ed.NewColumn() 552 } 553 } 554 555 func (ed *Editor) NewColumn() *ui.Column { 556 col := ed.UI.Root.Cols.NewColumn() 557 // close 558 col.EvReg.Add(ui.ColumnCloseEventId, func(ev0 interface{}) { 559 ed.EnsureOneColumn() 560 }) 561 return col 562 } 563 564 //---------- 565 566 func (ed *Editor) handleGlobalShortcuts(ev interface{}) (handled bool) { 567 switch t := ev.(type) { 568 case *event.WindowInput: 569 autoCloseInfo := true 570 571 switch t2 := t.Event.(type) { 572 case *event.KeyDown: 573 m := t2.Mods.ClearLocks() 574 switch { 575 case m.Is(event.ModNone): 576 switch t2.KeySym { 577 case event.KSymEscape: 578 ed.GoDebug.CancelAndClear() 579 ed.InlineComplete.CancelAndClear() 580 ed.cancelERowInfosCmds() 581 ed.cancelERowsContentCmds() 582 ed.cancelERowsInternalCmds() 583 autoCloseInfo = false 584 ed.cancelInfoFloatBox() 585 return true 586 case event.KSymF1: 587 autoCloseInfo = false 588 ed.toggleInfoFloatBox() 589 return true 590 } 591 } 592 } 593 594 if autoCloseInfo { 595 ed.UI.Root.ContextFloatBox.AutoClose(t.Event, t.Point) 596 if !ed.ifbw.ui().Visible() { 597 ed.cancelInfoFloatBox() 598 } 599 } 600 } 601 return false 602 } 603 604 //---------- 605 606 // example cmds canceled: openfilename, opensession, ... 607 func (ed *Editor) cancelERowsContentCmds() { 608 for _, erow := range ed.ERows() { 609 erow.CancelContentCmd() 610 } 611 } 612 613 // example cmds canceled: GoDebug, Lsproto*, ... 614 func (ed *Editor) cancelERowsInternalCmds() { 615 for _, erow := range ed.ERows() { 616 erow.CancelInternalCmd() 617 } 618 } 619 620 // example cmd canceled: presavehooks (goimports, src formatters, ...) 621 func (ed *Editor) cancelERowInfosCmds() { 622 for _, info := range ed.ERowInfos() { 623 info.CancelCmd() 624 } 625 } 626 627 //---------- 628 629 func (ed *Editor) cancelInfoFloatBox() { 630 ed.ifbw.Cancel() 631 cfb := ed.ifbw.ui() 632 cfb.Hide() 633 } 634 635 func (ed *Editor) toggleInfoFloatBox() { 636 ed.ifbw.Cancel() // cancel previous run 637 638 // toggle 639 cfb := ed.ifbw.ui() 640 cfb.Toggle() 641 if !cfb.Visible() { 642 return 643 } 644 645 // showInfoFloatBox 646 647 // find ta/erow under pointer 648 ta, ok := cfb.FindTextAreaUnderPointer() 649 if !ok { 650 cfb.Hide() 651 return 652 } 653 erow, ok := ed.NodeERow(ta) 654 if !ok { 655 cfb.Hide() 656 return 657 } 658 659 // show util 660 show := func(s string) { 661 cfb.TextArea.ClearPos() 662 cfb.SetStrClearHistory(s) 663 cfb.Show() 664 } 665 showAsync := func(s string) { 666 ed.UI.RunOnUIGoRoutine(func() { 667 if cfb.Visible() { 668 show(s) 669 } 670 }) 671 } 672 673 // initial ui feedback at position 674 cfb.SetRefPointToTextAreaCursor(ta) 675 show("Loading...") 676 677 ed.RunAsyncBusyCursor(cfb, func(done func()) { 678 defer done() 679 680 // there is no timeout to complete since the context can be canceled manually 681 682 // context based on erow context 683 ctx := ed.ifbw.NewCtx(erow.ctx) 684 685 // plugin autocomplete 686 showAsync("Loading plugin...") 687 err, handled := ed.Plugins.RunAutoComplete(ctx, cfb) 688 if handled { 689 if err != nil { 690 ed.Error(err) 691 } 692 return 693 } 694 695 // lsproto autocomplete 696 filename := "" 697 switch ta { 698 case erow.Row.TextArea: 699 if erow.Info.IsDir() { 700 filename = ".editor_directory" 701 } else { 702 filename = erow.Info.Name() 703 } 704 case erow.Row.Toolbar.TextArea: 705 filename = ".editor_toolbar" 706 default: 707 showAsync("") 708 return 709 } 710 // handle filename 711 lang, err := ed.LSProtoMan.LangManager(filename) 712 if err != nil { 713 showAsync(err.Error()) // err:"no registration for..." 714 return 715 } 716 // ui feedback while loading 717 v := fmt.Sprintf("Loading lsproto(%v)...", lang.Reg.Language) 718 showAsync(v) 719 // lsproto autocomplete 720 s, err := ed.lsprotoManAutoComplete(ctx, ta, erow) 721 if err != nil { 722 ed.Error(err) 723 showAsync("") 724 return 725 } 726 showAsync(s) 727 }) 728 } 729 730 func (ed *Editor) lsprotoManAutoComplete(ctx context.Context, ta *ui.TextArea, erow *ERow) (string, error) { 731 //ta := erow.Row.TextArea 732 comps, err := ed.LSProtoMan.TextDocumentCompletionDetailStrings(ctx, erow.Info.Name(), ta.RW(), ta.CursorIndex()) 733 if err != nil { 734 return "", err 735 } 736 s := "0 results" 737 if len(comps) > 0 { 738 s = strings.Join(comps, "\n") 739 } 740 return s, nil 741 } 742 743 //---------- 744 745 func (ed *Editor) NodeERow(node widget.Node) (*ERow, bool) { 746 for p := node.Embed().Parent; p != nil; p = p.Parent { 747 if r, ok := p.Wrapper.(*ui.Row); ok { 748 for _, erow := range ed.ERows() { 749 if r == erow.Row { 750 return erow, true 751 } 752 } 753 } 754 } 755 return nil, false 756 } 757 758 //---------- 759 760 // Caller should call done function in the end. 761 func (ed *Editor) RunAsyncBusyCursor(node widget.Node, fn func(done func())) { 762 set := func(c event.Cursor) { 763 ed.UI.RunOnUIGoRoutine(func() { 764 node.Embed().Cursor = c 765 ed.UI.QueueEmptyWindowInputEvent() // updates cursor tree 766 }) 767 } 768 set(event.WaitCursor) 769 done := func() { 770 set(event.NoneCursor) 771 } 772 // launch go routine to allow the UI to update the cursor 773 go fn(done) 774 } 775 776 //---------- 777 778 func (ed *Editor) SetAnnotations(req EdAnnotationsRequester, ta *ui.TextArea, on bool, selIndex int, entries []*drawer4.Annotation) { 779 if !ed.CanModifyAnnotations(req, ta) { 780 return 781 } 782 // set annotations (including clear) 783 if d, ok := ta.Drawer.(*drawer4.Drawer); ok { 784 d.Opt.Annotations.On = on 785 d.Opt.Annotations.Selected.EntryIndex = selIndex 786 d.Opt.Annotations.Entries = entries 787 ta.MarkNeedsLayoutAndPaint() 788 } 789 790 // restore godebug annotations 791 if req == EareqInlineComplete && !on { 792 ed.UI.RunOnUIGoRoutine(func() { // avoid lockup: godebugstart->inlinecomplete.clear->godebugrestoreannotations 793 // find erow info from textarea 794 for _, erow := range ed.ERows() { 795 if erow.Row.TextArea == ta { 796 ed.GoDebug.UpdateUIERowInfo(erow.Info) 797 } 798 } 799 }) 800 } 801 } 802 803 func (ed *Editor) CanModifyAnnotations(req EdAnnotationsRequester, ta *ui.TextArea) bool { 804 switch req { 805 case EareqGoDebugStart: 806 ed.InlineComplete.CancelAndClear() 807 return true 808 case EareqGoDebug: 809 if ed.InlineComplete.IsOn(ta) { 810 return false 811 } 812 return true 813 case EareqInlineComplete: 814 return true 815 default: 816 panic(req) 817 } 818 } 819 820 //---------- 821 822 func (ed *Editor) runPreSaveHooks(ctx context.Context, info *ERowInfo, b []byte) ([]byte, error) { 823 ext := filepath.Ext(info.Name()) 824 for _, h := range ed.preSaveHooks { 825 for _, e := range h.Exts { 826 if e == ext { 827 b2, err := ed.runPreSaveHook(ctx, info, b, h.Cmd) 828 if err != nil { 829 err2 := fmt.Errorf("presavehook(%v): %w", h.Language, err) 830 return nil, err2 831 } 832 b = b2 833 } 834 } 835 } 836 return b, nil 837 } 838 839 func (ed *Editor) runPreSaveHook(ctx context.Context, info *ERowInfo, content []byte, cmd string) ([]byte, error) { 840 // timeout for the cmd to run 841 timeout := 5 * time.Second 842 ctx2, cancel := context.WithTimeout(ctx, timeout) 843 defer cancel() 844 845 dir := filepath.Dir(info.Name()) 846 r := bytes.NewReader(content) 847 cmd2 := strings.Split(cmd, " ") 848 return ExecCmdStdin(ctx2, dir, r, cmd2...) 849 } 850 851 //---------- 852 853 type EdAnnotationsRequester int 854 855 const ( 856 EareqGoDebug EdAnnotationsRequester = iota 857 EareqGoDebugStart 858 EareqInlineComplete 859 ) 860 861 //---------- 862 863 type InfoFloatBoxWrap struct { 864 ed *Editor 865 ctx context.Context 866 canc context.CancelFunc 867 } 868 869 func NewInfoFloatBox(ed *Editor) *InfoFloatBoxWrap { 870 return &InfoFloatBoxWrap{ed: ed} 871 } 872 func (ifbw *InfoFloatBoxWrap) NewCtx(ctx context.Context) context.Context { 873 ifbw.Cancel() // cancel previous 874 ifbw.ctx, ifbw.canc = context.WithCancel(ctx) 875 return ifbw.ctx 876 } 877 func (ifbw *InfoFloatBoxWrap) Cancel() { 878 if ifbw.canc != nil { 879 ifbw.canc() 880 ifbw.canc = nil 881 } 882 } 883 func (ifbw *InfoFloatBoxWrap) ui() *ui.ContextFloatBox { 884 return ifbw.ed.UI.Root.ContextFloatBox 885 } 886 887 //---------- 888 889 type editorCloseEv struct{}