github.com/jmigpin/editor@v1.6.0/core/godebuginstance.go (about)

     1  package core
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/jmigpin/editor/core/godebug"
    16  	"github.com/jmigpin/editor/core/godebug/debug"
    17  	"github.com/jmigpin/editor/ui"
    18  	"github.com/jmigpin/editor/util/ctxutil"
    19  	"github.com/jmigpin/editor/util/drawutil/drawer4"
    20  	"github.com/jmigpin/editor/util/parseutil"
    21  )
    22  
    23  // Note: Should have a unique instance because there is no easy solution to debug two (or more) programs that have common files in the same editor
    24  
    25  const updatesPerSecond = 15
    26  
    27  type GoDebugManager struct {
    28  	ed   *Editor
    29  	inst struct {
    30  		sync.Mutex
    31  		inst   *GoDebugInstance
    32  		cancel context.CancelFunc
    33  	}
    34  }
    35  
    36  func NewGoDebugManager(ed *Editor) *GoDebugManager {
    37  	gdm := &GoDebugManager{ed: ed}
    38  	return gdm
    39  }
    40  
    41  func (gdm *GoDebugManager) Printf(format string, args ...interface{}) {
    42  	gdm.ed.Messagef("godebug: "+format, args...)
    43  }
    44  
    45  func (gdm *GoDebugManager) RunAsync(reqCtx context.Context, erow *ERow, args []string) error {
    46  
    47  	gdm.inst.Lock()
    48  	defer gdm.inst.Unlock()
    49  
    50  	// cancel and wait for previous
    51  	gdm.cancelAndWait()
    52  
    53  	// setup instance context // TODO: editor ctx?
    54  	ctx, cancel := context.WithCancel(context.Background())
    55  
    56  	// call cancel if reqCtx is done (short amount of time, just watches start)
    57  	clearWatching := ctxutil.WatchDone(cancel, reqCtx)
    58  	defer clearWatching()
    59  
    60  	inst, err := startGoDebugInstance(ctx, gdm.ed, gdm, erow, args)
    61  	if err != nil {
    62  		cancel()
    63  		return err
    64  	}
    65  
    66  	gdm.inst.inst = inst
    67  	gdm.inst.cancel = cancel
    68  	return nil
    69  }
    70  
    71  func (gdm *GoDebugManager) CancelAndClear() {
    72  	gdm.inst.Lock()
    73  	defer gdm.inst.Unlock()
    74  	gdm.cancelAndWait() // clears
    75  }
    76  func (gdm *GoDebugManager) cancelAndWait() {
    77  	if gdm.inst.inst != nil {
    78  		gdm.inst.cancel()
    79  		gdm.inst.inst.wait()
    80  		gdm.inst.inst = nil
    81  		gdm.inst.cancel = nil
    82  	}
    83  }
    84  
    85  func (gdm *GoDebugManager) SelectAnnotation(rowPos *ui.RowPos, ev *ui.RootSelectAnnotationEvent) {
    86  	gdm.inst.Lock()
    87  	defer gdm.inst.Unlock()
    88  	if gdm.inst.inst != nil {
    89  		gdm.inst.inst.selectAnnotation(rowPos, ev)
    90  	}
    91  }
    92  
    93  func (gdm *GoDebugManager) SelectERowAnnotation(erow *ERow, ev *ui.TextAreaSelectAnnotationEvent) {
    94  	gdm.inst.Lock()
    95  	defer gdm.inst.Unlock()
    96  	if gdm.inst.inst != nil {
    97  		gdm.inst.inst.selectERowAnnotation(erow, ev)
    98  	}
    99  }
   100  
   101  func (gdm *GoDebugManager) AnnotationFind(s string) error {
   102  	gdm.inst.Lock()
   103  	defer gdm.inst.Unlock()
   104  	if gdm.inst.inst == nil {
   105  		return fmt.Errorf("missing godebug instance")
   106  	}
   107  	return gdm.inst.inst.annotationFind(s)
   108  }
   109  
   110  func (gdm *GoDebugManager) UpdateUIERowInfo(info *ERowInfo) {
   111  	gdm.inst.Lock()
   112  	defer gdm.inst.Unlock()
   113  	if gdm.inst.inst != nil {
   114  		gdm.inst.inst.updateUIERowInfo(info)
   115  	}
   116  }
   117  
   118  //----------
   119  
   120  type GoDebugInstance struct {
   121  	ed           *Editor
   122  	gdm          *GoDebugManager
   123  	di           *GDDataIndex
   124  	erowExecWait sync.WaitGroup
   125  }
   126  
   127  func startGoDebugInstance(ctx context.Context, ed *Editor, gdm *GoDebugManager, erow *ERow, args []string) (*GoDebugInstance, error) {
   128  	gdi := &GoDebugInstance{ed: ed, gdm: gdm}
   129  	gdi.di = NewGDDataIndex(ed)
   130  	if err := gdi.start2(ctx, erow, args); err != nil {
   131  		return nil, err
   132  	}
   133  	return gdi, nil
   134  }
   135  
   136  func (gdi *GoDebugInstance) wait() {
   137  	gdi.erowExecWait.Wait()
   138  	gdi.clearInfosUI()
   139  }
   140  
   141  //----------
   142  
   143  func (gdi *GoDebugInstance) start2(ctx context.Context, erow *ERow, args []string) error {
   144  	// warn other annotators about starting a godebug session
   145  	_ = gdi.ed.CanModifyAnnotations(EareqGoDebugStart, erow.Row.TextArea)
   146  
   147  	// create new erow if necessary
   148  	if erow.Info.IsFileButNotDir() {
   149  		dir := filepath.Dir(erow.Info.Name())
   150  		info := erow.Ed.ReadERowInfo(dir)
   151  		rowPos := erow.Row.PosBelow()
   152  		erow = NewBasicERow(info, rowPos)
   153  	}
   154  
   155  	if !erow.Info.IsDir() {
   156  		return fmt.Errorf("can't run on this erow type")
   157  	}
   158  
   159  	gdi.erowExecWait.Add(1)
   160  	erow.Exec.RunAsync(func(erowCtx context.Context, rw io.ReadWriter) error {
   161  		defer gdi.erowExecWait.Done()
   162  
   163  		// call cancel if ctx is done (allow cancel from godebugmanager)
   164  		erowCtx2, cancel := context.WithCancel(erowCtx)
   165  		defer cancel()
   166  		clearWatching := ctxutil.WatchDone(cancel, ctx)
   167  		defer clearWatching()
   168  
   169  		return gdi.runCmd(erowCtx2, erow, args, rw)
   170  	})
   171  
   172  	return nil
   173  }
   174  
   175  func (gdi *GoDebugInstance) runCmd(ctx context.Context, erow *ERow, args []string, w io.Writer) error {
   176  	cmd := godebug.NewCmd()
   177  
   178  	cmd.Dir = erow.Info.Name()
   179  	cmd.Stdout = w
   180  	cmd.Stderr = w
   181  
   182  	done, err := cmd.Start(ctx, args[1:])
   183  	if err != nil {
   184  		return err
   185  	}
   186  	if done {
   187  		return nil
   188  	}
   189  
   190  	if err := gdi.di.handleFilesDataMsg(cmd.FilesData()); err != nil {
   191  		return err
   192  	}
   193  
   194  	gdi.clientMsgsLoop(ctx, w, cmd) // blocking
   195  
   196  	return cmd.Wait()
   197  }
   198  
   199  //----------
   200  
   201  func (gdi *GoDebugInstance) selectERowAnnotation(erow *ERow, ev *ui.TextAreaSelectAnnotationEvent) {
   202  	if gdi.selectERowAnnotation2(erow, ev) {
   203  		gdi.updateUIShowLine(erow.Row.PosBelow())
   204  	}
   205  }
   206  
   207  func (gdi *GoDebugInstance) selectERowAnnotation2(erow *ERow, ev *ui.TextAreaSelectAnnotationEvent) bool {
   208  	switch ev.Type {
   209  	case ui.TASelAnnTypeCurrent,
   210  		ui.TASelAnnTypeCurrentPrev,
   211  		ui.TASelAnnTypeCurrentNext:
   212  		return gdi.di.annMsgChangeCurrent(erow.Info.Name(), ev.AnnotationIndex, ev.Type)
   213  	case ui.TASelAnnTypePrint:
   214  		gdi.printIndex(erow, ev.AnnotationIndex, ev.Offset)
   215  		return false
   216  	case ui.TASelAnnTypePrintAllPrevious:
   217  		gdi.printIndexAllPrevious(erow, ev.AnnotationIndex, ev.Offset)
   218  		return false
   219  	default:
   220  		log.Printf("todo: %#v", ev)
   221  	}
   222  	return false
   223  }
   224  
   225  //----------
   226  
   227  func (gdi *GoDebugInstance) annotationFind(s string) error {
   228  	_, ok := gdi.di.selectedAnnFind(s)
   229  	if !ok {
   230  		return fmt.Errorf("string not found in selected annotation: %v", s)
   231  	}
   232  	gdi.updateUIShowLine(gdi.ed.GoodRowPos())
   233  	return nil
   234  }
   235  
   236  //----------
   237  
   238  func (gdi *GoDebugInstance) selectAnnotation(rowPos *ui.RowPos, ev *ui.RootSelectAnnotationEvent) {
   239  	if gdi.selectAnnotation2(ev) {
   240  		gdi.updateUIShowLine(rowPos)
   241  	}
   242  }
   243  
   244  func (gdi *GoDebugInstance) selectAnnotation2(ev *ui.RootSelectAnnotationEvent) bool {
   245  	switch ev.Type {
   246  	case ui.RootSelAnnTypeFirst:
   247  		_ = gdi.di.selectFirst()
   248  		gdi.openArrivalIndexERow()
   249  		return true // show always
   250  	case ui.RootSelAnnTypeLast:
   251  		_ = gdi.di.selectLast()
   252  		gdi.openArrivalIndexERow()
   253  		return true // show always
   254  	case ui.RootSelAnnTypePrev:
   255  		_ = gdi.di.selectPrev()
   256  		gdi.openArrivalIndexERow()
   257  		return true // show always
   258  	case ui.RootSelAnnTypeNext:
   259  		_ = gdi.di.selectNext()
   260  		gdi.openArrivalIndexERow()
   261  		return true // show always
   262  	case ui.RootSelAnnTypeClear:
   263  		gdi.di.clearMsgs()
   264  		return true
   265  	default:
   266  		log.Printf("todo: %#v", ev)
   267  	}
   268  	return false
   269  }
   270  
   271  //----------
   272  
   273  func (gdi *GoDebugInstance) printIndex(erow *ERow, annIndex, offset int) {
   274  	msg, ok := gdi.di.annMsg(erow.Info.Name(), annIndex)
   275  	if !ok {
   276  		return
   277  	}
   278  	// build output
   279  	s := godebug.StringifyItemFull(msg.dbgLineMsg.Item)
   280  	gdi.gdm.Printf("annotation: #%d\n\t%v\n", msg.arrivalIndex, s)
   281  }
   282  
   283  func (gdi *GoDebugInstance) printIndexAllPrevious(erow *ERow, annIndex, offset int) {
   284  	msgs, ok := gdi.di.annPreviousMsgs(erow.Info.Name(), annIndex)
   285  	if !ok {
   286  		return
   287  	}
   288  	// build output
   289  	sb := strings.Builder{}
   290  	for _, msg := range msgs {
   291  		s := godebug.StringifyItemFull(msg.dbgLineMsg.Item)
   292  		sb.WriteString(fmt.Sprintf("\t" + s + "\n"))
   293  	}
   294  	gdi.gdm.Printf("annotations (%d entries):\n%v\n", len(msgs), sb.String())
   295  }
   296  
   297  //----------
   298  
   299  func (gdi *GoDebugInstance) clientMsgsLoop(ctx context.Context, w io.Writer, cmd *godebug.Cmd) {
   300  	var updatec <-chan time.Time // update channel
   301  	updateUI := func() {
   302  		if updatec != nil {
   303  			updatec = nil
   304  			gdi.updateUI()
   305  		}
   306  	}
   307  
   308  	for {
   309  		select {
   310  		case <-ctx.Done():
   311  			updateUI() // final ui update
   312  			return
   313  		case msg, ok := <-cmd.Client.Messages:
   314  			if !ok {
   315  				updateUI() // last msg (end of program), final ui update
   316  				return
   317  			}
   318  			if err := gdi.handleMsg(msg, cmd); err != nil {
   319  				fmt.Fprintf(w, "error: %v\n", err)
   320  			}
   321  			if updatec == nil {
   322  				t := time.NewTimer(time.Second / updatesPerSecond)
   323  				updatec = t.C
   324  			}
   325  		case <-updatec:
   326  			updateUI()
   327  		}
   328  	}
   329  }
   330  
   331  //----------
   332  
   333  func (gdi *GoDebugInstance) handleMsg(msg interface{}, cmd *godebug.Cmd) error {
   334  	switch t := msg.(type) {
   335  	case error:
   336  		return t
   337  	case *debug.LineMsg:
   338  		return gdi.di.handleLineMsgs(t)
   339  	case []*debug.LineMsg:
   340  		return gdi.di.handleLineMsgs(t...)
   341  	default:
   342  		return fmt.Errorf("unexpected msg: %T", msg)
   343  	}
   344  	return nil
   345  }
   346  
   347  //----------
   348  
   349  func (gdi *GoDebugInstance) updateUI() {
   350  	gdi.ed.UI.RunOnUIGoRoutine(func() {
   351  		gdi.updateUI2()
   352  	})
   353  }
   354  
   355  func (gdi *GoDebugInstance) updateUIShowLine(rowPos *ui.RowPos) {
   356  	gdi.ed.UI.RunOnUIGoRoutine(func() {
   357  		gdi.updateUI2()
   358  		gdi.showSelectedLine(rowPos)
   359  	})
   360  }
   361  
   362  func (gdi *GoDebugInstance) updateUIERowInfo(info *ERowInfo) {
   363  	gdi.ed.UI.RunOnUIGoRoutine(func() {
   364  		gdi.updateInfoUI(info)
   365  	})
   366  }
   367  
   368  //----------
   369  
   370  func (gdi *GoDebugInstance) clearInfosUI() {
   371  	gdi.ed.UI.RunOnUIGoRoutine(func() {
   372  		for _, info := range gdi.ed.ERowInfos() {
   373  			gdi.clearInfoUI(info)
   374  		}
   375  	})
   376  }
   377  
   378  func (gdi *GoDebugInstance) clearInfoUI(info *ERowInfo) {
   379  	info.UpdateAnnotationsRowState(false)
   380  	info.UpdateAnnotationsEditedRowState(false)
   381  	gdi.clearAnnotations(info)
   382  }
   383  
   384  //----------
   385  
   386  func (gdi *GoDebugInstance) updateUI2() {
   387  	for _, info := range gdi.ed.ERowInfos() {
   388  		gdi.updateInfoUI(info)
   389  	}
   390  }
   391  
   392  func (gdi *GoDebugInstance) updateInfoUI(info *ERowInfo) {
   393  	// TODO: the info should be get with one locked call to the dataindex
   394  
   395  	// Note: the current selected debug line might not have an open erow (ex: when auto increased to match the lastarrivalindex).
   396  
   397  	// file belongs to the godebug session
   398  	findex, ok := gdi.di.FilesIndex(info.Name())
   399  	if !ok {
   400  		info.UpdateAnnotationsRowState(false)
   401  		info.UpdateAnnotationsEditedRowState(false)
   402  		gdi.clearAnnotations(info)
   403  		return
   404  	}
   405  	info.UpdateAnnotationsRowState(true)
   406  
   407  	// check if content has changed
   408  	edited := gdi.di.updateFileEdited(info)
   409  	if edited {
   410  		info.UpdateAnnotationsEditedRowState(true)
   411  		gdi.clearAnnotations(info)
   412  		return
   413  	}
   414  	info.UpdateAnnotationsEditedRowState(false)
   415  
   416  	selLine, ok := gdi.di.findSelectedAndUpdateAnnEntries(findex)
   417  	if !ok {
   418  		selLine = -1
   419  	}
   420  
   421  	// set annotations
   422  	file := gdi.di.files[findex] // TODO: not locked (file.AnnEntries used)
   423  	for _, erow := range info.ERows {
   424  		gdi.setAnnotations(erow, true, selLine, file.annEntries)
   425  	}
   426  }
   427  
   428  func (gdi *GoDebugInstance) clearAnnotations(info *ERowInfo) {
   429  	for _, erow := range info.ERows {
   430  		gdi.setAnnotations(erow, false, -1, nil)
   431  	}
   432  }
   433  
   434  func (gdi *GoDebugInstance) setAnnotations(erow *ERow, on bool, selIndex int, entries []*drawer4.Annotation) {
   435  	gdi.ed.SetAnnotations(EareqGoDebug, erow.Row.TextArea, on, selIndex, entries)
   436  }
   437  
   438  //----------
   439  
   440  func (gdi *GoDebugInstance) showSelectedLine(rowPos *ui.RowPos) {
   441  	msg, filename, arrivalIndex, edited, ok := gdi.di.selectedMsg()
   442  	if !ok {
   443  		return
   444  	}
   445  
   446  	// TODO: don't show if on UI list, show warnings about skipped steps
   447  	// some rows show because the selected arrival index is just increased
   448  	// but in the case of searching for the next selected arrival index, if the info row is not opened, it doesn't search inside that file, and so the index stays the same as the last selected index
   449  
   450  	// don't show on edited files
   451  	if edited {
   452  		gdi.ed.Errorf("selection at edited row: %v: step %v", filename, arrivalIndex)
   453  		return
   454  	}
   455  
   456  	// file offset
   457  	dlm := msg.dbgLineMsg
   458  	fo := &parseutil.FilePos{Filename: filename, Offset: dlm.Offset}
   459  
   460  	// show line
   461  	conf := &OpenFileERowConfig{
   462  		FilePos:             fo,
   463  		RowPos:              rowPos,
   464  		FlashVisibleOffsets: true,
   465  		NewIfNotExistent:    true,
   466  	}
   467  	OpenFileERow(gdi.ed, conf)
   468  }
   469  
   470  //----------
   471  
   472  func (gdi *GoDebugInstance) openArrivalIndexERow() {
   473  	_, filename, ok := gdi.di.selectedArrivalIndexFilename()
   474  	if !ok {
   475  		return
   476  	}
   477  
   478  	rowPos := gdi.ed.GoodRowPos()
   479  	conf := &OpenFileERowConfig{
   480  		FilePos:          &parseutil.FilePos{Filename: filename},
   481  		RowPos:           rowPos,
   482  		CancelIfExistent: true,
   483  		NewIfNotExistent: true,
   484  	}
   485  	gdi.ed.UI.RunOnUIGoRoutine(func() {
   486  		OpenFileERow(gdi.ed, conf)
   487  	})
   488  }
   489  
   490  //----------
   491  
   492  // GoDebug data Index
   493  type GDDataIndex struct {
   494  	sync.RWMutex // used internally, not to be locked outside
   495  
   496  	ed          *Editor
   497  	filesIndexM map[string]int // [name]fileindex
   498  	filesEdited map[int]bool   // [fileindex]
   499  
   500  	afds  []*debug.AnnotatorFileData // [fileindex]
   501  	files []*GDFileMsgs              // [fileindex]
   502  
   503  	lastArrivalIndex int
   504  	selected         struct {
   505  		arrivalIndex  int
   506  		fileIndex     int
   507  		lineIndex     int
   508  		lineStepIndex int
   509  	}
   510  }
   511  
   512  func NewGDDataIndex(ed *Editor) *GDDataIndex {
   513  	di := &GDDataIndex{ed: ed}
   514  	di.filesIndexM = map[string]int{}
   515  	di.filesEdited = map[int]bool{}
   516  	di.clearMsgs()
   517  	return di
   518  }
   519  
   520  func (di *GDDataIndex) FilesIndex(name string) (int, bool) {
   521  	name = di.FilesIndexKey(name)
   522  	v, ok := di.filesIndexM[name]
   523  	return v, ok
   524  }
   525  func (di *GDDataIndex) FilesIndexKey(name string) string {
   526  	if di.ed.FsCaseInsensitive {
   527  		name = strings.ToLower(name)
   528  	}
   529  	return name
   530  }
   531  
   532  func (di *GDDataIndex) clearMsgs() {
   533  	di.Lock()
   534  	defer di.Unlock()
   535  	for _, f := range di.files {
   536  		n := len(f.linesMsgs) // keep n
   537  		u := NewGDFileMsgs(n)
   538  		*f = *u
   539  	}
   540  	di.lastArrivalIndex = -1
   541  	di.selected.arrivalIndex = di.lastArrivalIndex
   542  }
   543  
   544  //----------
   545  
   546  func (di *GDDataIndex) handleFilesDataMsg(fdm *debug.FilesDataMsg) error {
   547  	di.Lock()
   548  	defer di.Unlock()
   549  
   550  	di.afds = fdm.Data
   551  	// index filenames
   552  	di.filesIndexM = map[string]int{}
   553  	for _, afd := range di.afds {
   554  		name := di.FilesIndexKey(afd.Filename)
   555  		di.filesIndexM[name] = afd.FileIndex
   556  	}
   557  	// init index
   558  	di.files = make([]*GDFileMsgs, len(di.afds))
   559  	for _, afd := range di.afds {
   560  		// check index
   561  		if afd.FileIndex >= len(di.files) {
   562  			return fmt.Errorf("bad file index at init: %v len=%v", afd.FileIndex, len(di.files))
   563  		}
   564  		di.files[afd.FileIndex] = NewGDFileMsgs(afd.DebugLen)
   565  	}
   566  	return nil
   567  }
   568  
   569  func (di *GDDataIndex) handleLineMsgs(msgs ...*debug.LineMsg) error {
   570  	di.Lock()
   571  	defer di.Unlock()
   572  	for _, msg := range msgs {
   573  		err := di._handleLineMsg(msg)
   574  		if err != nil {
   575  			return err
   576  		}
   577  	}
   578  	return nil
   579  }
   580  
   581  // Not locked
   582  func (di *GDDataIndex) _handleLineMsg(u *debug.LineMsg) error {
   583  	// check index
   584  	l1 := len(di.files)
   585  	if u.FileIndex >= l1 {
   586  		return fmt.Errorf("bad file index: %v len=%v", u.FileIndex, l1)
   587  	}
   588  	// check index
   589  	l2 := len(di.files[u.FileIndex].linesMsgs)
   590  	if u.DebugIndex >= l2 {
   591  		return fmt.Errorf("bad debug index: %v len=%v", u.DebugIndex, l2)
   592  	}
   593  	// line msg
   594  	di.lastArrivalIndex++ // starts/clears to -1, so first n is 0
   595  	lm := &GDLineMsg{arrivalIndex: di.lastArrivalIndex, dbgLineMsg: u}
   596  	// index msg
   597  	w := &di.files[u.FileIndex].linesMsgs[u.DebugIndex].lineMsgs
   598  	*w = append(*w, lm)
   599  	// mark file as having new data (performance)
   600  	//di.files[u.FileIndex].hasNewData = true
   601  
   602  	// auto update selected index if at last position
   603  	if di.selected.arrivalIndex == di.lastArrivalIndex-1 {
   604  		di.selected.arrivalIndex = di.lastArrivalIndex
   605  	}
   606  
   607  	return nil
   608  }
   609  
   610  //----------
   611  
   612  func (di *GDDataIndex) annMsg(filename string, annIndex int) (*GDLineMsg, bool) {
   613  	di.RLock()
   614  	defer di.RUnlock()
   615  
   616  	file, line, ok := di._annIndexFileLine(filename, annIndex)
   617  	if !ok {
   618  		return nil, false
   619  	}
   620  	// current msg index at line
   621  	k := file.annEntriesLMIndex[annIndex] // same length as lineMsgs
   622  	if k < 0 || k >= len(line.lineMsgs) { // currently nothing is shown or cleared
   623  		return nil, false
   624  	}
   625  	return line.lineMsgs[k], true
   626  }
   627  
   628  func (di *GDDataIndex) annPreviousMsgs(filename string, annIndex int) ([]*GDLineMsg, bool) {
   629  	di.RLock()
   630  	defer di.RUnlock()
   631  
   632  	file, line, ok := di._annIndexFileLine(filename, annIndex)
   633  	if !ok {
   634  		return nil, false
   635  	}
   636  	// current msg index at line
   637  	k := file.annEntriesLMIndex[annIndex] // same length as lineMsgs
   638  	if k < 0 || k >= len(line.lineMsgs) { // currently nothing is shown or cleared
   639  		return nil, false
   640  	}
   641  	return line.lineMsgs[:k+1], true
   642  }
   643  
   644  func (di *GDDataIndex) selectedAnnFind(s string) (*GDLineMsg, bool) {
   645  	di.RLock()
   646  	defer di.RUnlock()
   647  
   648  	annIndex, filename, ok := di.selectedArrivalIndexFilename()
   649  	if !ok {
   650  		return nil, false
   651  	}
   652  
   653  	file, line, ok := di._annIndexFileLine(filename, annIndex)
   654  	if !ok {
   655  		return nil, false
   656  	}
   657  
   658  	b := []byte(s)
   659  	k := file.annEntriesLMIndex[annIndex] // current entry
   660  	for i := 0; i < len(line.lineMsgs); i++ {
   661  		h := (k + 1 + i) % len(line.lineMsgs)
   662  		msg := line.lineMsgs[h]
   663  		ann := msg.annotation()
   664  		j := bytes.Index(ann.Bytes, b)
   665  		if j >= 0 {
   666  			di.selected.arrivalIndex = msg.arrivalIndex
   667  			return msg, true
   668  		}
   669  	}
   670  
   671  	return nil, false
   672  }
   673  
   674  func (di *GDDataIndex) annMsgChangeCurrent(filename string, annIndex int, typ ui.TASelAnnType) bool {
   675  	di.Lock() // writes di.selected
   676  	defer di.Unlock()
   677  
   678  	file, line, ok := di._annIndexFileLine(filename, annIndex)
   679  	if !ok {
   680  		return false
   681  	}
   682  	// current msg index at line
   683  	k := file.annEntriesLMIndex[annIndex] // same length as lineMsgs
   684  
   685  	// adjust k according to type
   686  	switch typ {
   687  	case ui.TASelAnnTypeCurrent:
   688  		// allow to select first if no line is visible
   689  		if k < 0 {
   690  			k = 0
   691  		}
   692  	case ui.TASelAnnTypeCurrentPrev:
   693  		k--
   694  	case ui.TASelAnnTypeCurrentNext:
   695  		k++
   696  	default:
   697  		panic(fmt.Sprintf("unexpected type: %v", typ))
   698  	}
   699  
   700  	if k < 0 || k >= len(line.lineMsgs) { // currently nothing is shown or cleared
   701  		return false
   702  	}
   703  	di.selected.arrivalIndex = line.lineMsgs[k].arrivalIndex
   704  	return true
   705  }
   706  
   707  // Not locked
   708  func (di *GDDataIndex) _annIndexFileLine(filename string, annIndex int) (*GDFileMsgs, *GDLineMsgs, bool) {
   709  	// file
   710  	findex, ok := di.FilesIndex(filename)
   711  	if !ok {
   712  		return nil, nil, false
   713  	}
   714  	file := di.files[findex]
   715  	// line
   716  	if annIndex < 0 || annIndex >= len(file.linesMsgs) {
   717  		return nil, nil, false
   718  	}
   719  	return file, &file.linesMsgs[annIndex], true
   720  }
   721  
   722  //----------
   723  
   724  func (di *GDDataIndex) selectFirst() bool {
   725  	di.Lock()
   726  	defer di.Unlock()
   727  	if di.selected.arrivalIndex != 0 && 0 <= di.lastArrivalIndex { // could be -1
   728  		di.selected.arrivalIndex = 0
   729  		return true
   730  	}
   731  	return false
   732  }
   733  
   734  func (di *GDDataIndex) selectLast() bool {
   735  	di.Lock()
   736  	defer di.Unlock()
   737  	if di.selected.arrivalIndex != di.lastArrivalIndex {
   738  		di.selected.arrivalIndex = di.lastArrivalIndex
   739  		return true
   740  	}
   741  	return false
   742  }
   743  
   744  func (di *GDDataIndex) selectPrev() bool {
   745  	di.Lock()
   746  	defer di.Unlock()
   747  	if di.selected.arrivalIndex > 0 {
   748  		di.selected.arrivalIndex--
   749  		return true
   750  	}
   751  	return false
   752  }
   753  
   754  func (di *GDDataIndex) selectNext() bool {
   755  	di.Lock()
   756  	defer di.Unlock()
   757  	if di.selected.arrivalIndex < di.lastArrivalIndex {
   758  		di.selected.arrivalIndex++
   759  		return true
   760  	}
   761  	return false
   762  }
   763  
   764  //----------
   765  
   766  func (di *GDDataIndex) findSelectedAndUpdateAnnEntries(findex int) (int, bool) {
   767  	di.Lock()
   768  	defer di.Unlock()
   769  	file := di.files[findex]
   770  	selLine, selLineStep, selFound := file._findSelectedAndUpdateAnnEntries(di.selected.arrivalIndex)
   771  	if selFound {
   772  		di.selected.fileIndex = findex
   773  		di.selected.lineIndex = selLine
   774  		di.selected.lineStepIndex = selLineStep
   775  	}
   776  	return selLine, selFound
   777  }
   778  
   779  //----------
   780  
   781  func (di *GDDataIndex) selectedMsg() (*GDLineMsg, string, int, bool, bool) {
   782  	di.RLock()
   783  	defer di.RUnlock()
   784  
   785  	msg, ok := di._selectedMsg2()
   786  	if !ok {
   787  		return nil, "", 0, false, false
   788  	}
   789  
   790  	findex := di.selected.fileIndex
   791  	filename := di.afds[findex].Filename
   792  	edited := di.filesEdited[findex]
   793  	return msg, filename, di.selected.arrivalIndex, edited, true
   794  }
   795  
   796  // Not locked.
   797  func (di *GDDataIndex) _selectedMsg2() (*GDLineMsg, bool) {
   798  	// in case of a clear
   799  	if di.selected.arrivalIndex < 0 {
   800  		return nil, false
   801  	}
   802  
   803  	findex := di.selected.fileIndex
   804  	if findex < 0 || findex >= len(di.files) {
   805  		return nil, false
   806  	}
   807  	file := di.files[findex]
   808  
   809  	lineIndex := di.selected.lineIndex
   810  	if lineIndex < 0 || lineIndex >= len(file.linesMsgs) {
   811  		return nil, false
   812  	}
   813  	lm := file.linesMsgs[lineIndex]
   814  
   815  	stepIndex := di.selected.lineStepIndex
   816  	if stepIndex < 0 || stepIndex >= len(lm.lineMsgs) {
   817  		return nil, false
   818  	}
   819  
   820  	return lm.lineMsgs[stepIndex], true
   821  }
   822  
   823  //----------
   824  
   825  func (di *GDDataIndex) updateFileEdited(info *ERowInfo) bool {
   826  	di.Lock()
   827  	defer di.Unlock()
   828  	findex, ok := di.FilesIndex(info.Name())
   829  	if !ok {
   830  		return false
   831  	}
   832  	afd := di.afds[findex]
   833  	edited := !info.EqualToBytesHash(afd.FileSize, afd.FileHash)
   834  	di.filesEdited[findex] = edited
   835  	return edited
   836  }
   837  
   838  func (di *GDDataIndex) isFileEdited(filename string) bool {
   839  	di.RLock()
   840  	defer di.RUnlock()
   841  	findex, ok := di.FilesIndex(filename)
   842  	if !ok {
   843  		return false
   844  	}
   845  	return di.filesEdited[findex]
   846  }
   847  
   848  //----------
   849  
   850  func (di *GDDataIndex) selectedArrivalIndexFilename() (int, string, bool) {
   851  	return di.arrivalIndexFilename(di.selected.arrivalIndex)
   852  }
   853  
   854  func (di *GDDataIndex) arrivalIndexFilename(arrivalIndex int) (int, string, bool) {
   855  	di.RLock()
   856  	defer di.RUnlock()
   857  	for findex, file := range di.files {
   858  		for j, lm := range file.linesMsgs {
   859  			_, eqK, _ := lm.findIndex(arrivalIndex)
   860  			if eqK {
   861  				return j, di.afds[findex].Filename, true
   862  			}
   863  		}
   864  	}
   865  	return -1, "", false
   866  }
   867  
   868  //----------
   869  
   870  type GDFileMsgs struct {
   871  	linesMsgs []GDLineMsgs // [lineIndex] file annotations received
   872  
   873  	// current annotation entries to be shown with a file
   874  	annEntries        []*drawer4.Annotation
   875  	annEntriesLMIndex []int // [lineIndex]stepIndex: line messages index: keep selected step index to know the msg entry when coming from a click on an annotation
   876  
   877  	//hasNewData bool // performance
   878  }
   879  
   880  func NewGDFileMsgs(n int) *GDFileMsgs {
   881  	return &GDFileMsgs{
   882  		linesMsgs:         make([]GDLineMsgs, n),
   883  		annEntries:        make([]*drawer4.Annotation, n),
   884  		annEntriesLMIndex: make([]int, n),
   885  	}
   886  }
   887  
   888  // Not locked
   889  func (file *GDFileMsgs) _findSelectedAndUpdateAnnEntries(arrivalIndex int) (int, int, bool) {
   890  	found := false
   891  	selLine := 0
   892  	selLineStep := 0
   893  	for line, lm := range file.linesMsgs {
   894  		k, eqK, foundK := lm.findIndex(arrivalIndex)
   895  		if foundK {
   896  			file.annEntries[line] = lm.lineMsgs[k].annotation()
   897  			file.annEntriesLMIndex[line] = k
   898  			if eqK {
   899  				found = true
   900  				selLine = line
   901  				selLineStep = k
   902  			}
   903  		} else {
   904  			if len(lm.lineMsgs) > 0 {
   905  				file.annEntries[line] = lm.lineMsgs[0].emptyAnnotation()
   906  			} else {
   907  				file.annEntries[line] = nil // no msgs ever received
   908  			}
   909  			file.annEntriesLMIndex[line] = -1
   910  		}
   911  	}
   912  	return selLine, selLineStep, found
   913  }
   914  
   915  //----------
   916  
   917  type GDLineMsgs struct {
   918  	lineMsgs []*GDLineMsg // [arrivalIndex] line annotations received
   919  }
   920  
   921  func (lm *GDLineMsgs) findIndex(arrivalIndex int) (int, bool, bool) {
   922  	k := sort.Search(len(lm.lineMsgs), func(i int) bool {
   923  		u := lm.lineMsgs[i].arrivalIndex
   924  		return u >= arrivalIndex
   925  	})
   926  	foundK := false
   927  	eqK := false
   928  	if k < len(lm.lineMsgs) && lm.lineMsgs[k].arrivalIndex == arrivalIndex {
   929  		eqK = true
   930  		foundK = true
   931  	} else {
   932  		k-- // current k is above arrivalIndex, want previous
   933  		foundK = k >= 0
   934  	}
   935  	return k, eqK, foundK
   936  }
   937  
   938  //----------
   939  
   940  type GDLineMsg struct {
   941  	arrivalIndex int
   942  	dbgLineMsg   *debug.LineMsg
   943  	cache        struct {
   944  		item []byte
   945  		ann  *drawer4.Annotation
   946  	}
   947  }
   948  
   949  func (msg *GDLineMsg) ann() *drawer4.Annotation {
   950  	if msg.cache.ann == nil {
   951  		msg.cache.ann = &drawer4.Annotation{Offset: msg.dbgLineMsg.Offset}
   952  	}
   953  	return msg.cache.ann
   954  }
   955  
   956  func (msg *GDLineMsg) annotation() *drawer4.Annotation {
   957  	ann := msg.ann()
   958  	if msg.cache.item == nil {
   959  		s := godebug.StringifyItem(msg.dbgLineMsg.Item)
   960  		msg.cache.item = []byte(s)
   961  	}
   962  	ann.Bytes = msg.cache.item
   963  	ann.NotesBytes = []byte(fmt.Sprintf("#%d", msg.arrivalIndex))
   964  	return ann
   965  }
   966  
   967  func (msg *GDLineMsg) emptyAnnotation() *drawer4.Annotation {
   968  	ann := msg.ann()
   969  	ann.Bytes = []byte(" ")
   970  	ann.NotesBytes = nil
   971  	return ann
   972  }