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{}