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

     1  package core
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"sync"
     7  	"unicode"
     8  
     9  	"github.com/jmigpin/editor/ui"
    10  	"github.com/jmigpin/editor/util/drawutil/drawer4"
    11  	"github.com/jmigpin/editor/util/iout/iorw"
    12  	"github.com/jmigpin/editor/util/parseutil"
    13  )
    14  
    15  type InlineComplete struct {
    16  	ed *Editor
    17  
    18  	mu struct {
    19  		sync.Mutex
    20  		cancel context.CancelFunc
    21  		ta     *ui.TextArea // if not nil, inlinecomplete is on
    22  		index  int          // cursor index
    23  	}
    24  }
    25  
    26  func NewInlineComplete(ed *Editor) *InlineComplete {
    27  	ic := &InlineComplete{ed: ed}
    28  	ic.mu.cancel = func() {} // avoid testing for nil
    29  	return ic
    30  }
    31  
    32  //----------
    33  
    34  func (ic *InlineComplete) Complete(erow *ERow, ev *ui.TextAreaInlineCompleteEvent) bool {
    35  
    36  	// early pre-check if filename is supported
    37  	_, err := ic.ed.LSProtoMan.LangManager(erow.Info.Name())
    38  	if err != nil {
    39  		return false // not handled
    40  	}
    41  
    42  	ta := ev.TextArea
    43  
    44  	ic.mu.Lock()
    45  	ic.mu.cancel() // cancel previous run
    46  	ctx, cancel := context.WithCancel(erow.ctx)
    47  	ic.mu.cancel = cancel
    48  	ic.mu.ta = ta
    49  	ic.mu.index = ta.CursorIndex()
    50  	ic.mu.Unlock()
    51  
    52  	go func() {
    53  		defer cancel()
    54  		ic.setAnnotationsMsg(ta, "loading...")
    55  		err := ic.complete2(ctx, erow.Info.Name(), ta, ev)
    56  		if err != nil {
    57  			ic.setAnnotations(ta, nil)
    58  			ic.ed.Error(err)
    59  		}
    60  		// TODO: not necessary in all cases
    61  		// ensure UI update
    62  		ic.ed.UI.EnqueueNoOpEvent()
    63  	}()
    64  	return true
    65  }
    66  
    67  func (ic *InlineComplete) complete2(ctx context.Context, filename string, ta *ui.TextArea, ev *ui.TextAreaInlineCompleteEvent) error {
    68  	comps, err := ic.completions(ctx, filename, ta)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	// insert complete
    74  	completed, comps, err := ic.insertComplete(comps, ta)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	switch len(comps) {
    80  	case 0:
    81  		ic.setAnnotationsMsg(ta, "0 results")
    82  	case 1:
    83  		if completed {
    84  			ic.setAnnotations(ta, nil)
    85  		} else {
    86  			ic.setAnnotationsMsg(ta, "already complete")
    87  		}
    88  	default:
    89  		// show completions
    90  		entries := []*drawer4.Annotation{}
    91  		for _, v := range comps {
    92  			u := &drawer4.Annotation{Offset: ev.Offset, Bytes: []byte(v)}
    93  			entries = append(entries, u)
    94  		}
    95  		ic.setAnnotations(ta, entries)
    96  	}
    97  	return nil
    98  }
    99  
   100  func (ic *InlineComplete) insertComplete(comps []string, ta *ui.TextArea) (completed bool, _ []string, _ error) {
   101  	ta.BeginUndoGroup()
   102  	defer ta.EndUndoGroup()
   103  	newIndex, completed, comps2, err := insertComplete(comps, ta.RW(), ta.CursorIndex())
   104  	if err != nil {
   105  		return completed, comps2, err
   106  	}
   107  	if newIndex != 0 {
   108  		ta.SetCursorIndex(newIndex)
   109  		// update index for CancelOnCursorChange
   110  		ic.mu.Lock()
   111  		ic.mu.index = newIndex
   112  		ic.mu.Unlock()
   113  	}
   114  	return completed, comps2, err
   115  }
   116  
   117  //----------
   118  
   119  func (ic *InlineComplete) completions(ctx context.Context, filename string, ta *ui.TextArea) ([]string, error) {
   120  	compList, err := ic.ed.LSProtoMan.TextDocumentCompletion(ctx, filename, ta.RW(), ta.CursorIndex())
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	res := []string{}
   125  	for _, ci := range compList.Items {
   126  		// trim labels (clangd: has some entries prefixed with space)
   127  		label := strings.TrimSpace(ci.Label)
   128  
   129  		res = append(res, label)
   130  	}
   131  	return res, nil
   132  }
   133  
   134  //----------
   135  
   136  func (ic *InlineComplete) setAnnotationsMsg(ta *ui.TextArea, s string) {
   137  	offset := ta.CursorIndex()
   138  	entries := []*drawer4.Annotation{{Offset: offset, Bytes: []byte(s)}}
   139  	ic.setAnnotations(ta, entries)
   140  }
   141  
   142  func (ic *InlineComplete) setAnnotations(ta *ui.TextArea, entries []*drawer4.Annotation) {
   143  	on := entries != nil && len(entries) > 0
   144  	ic.ed.SetAnnotations(EareqInlineComplete, ta, on, -1, entries)
   145  	if !on {
   146  		ic.setOff(ta)
   147  	}
   148  }
   149  
   150  //----------
   151  
   152  func (ic *InlineComplete) IsOn(ta *ui.TextArea) bool {
   153  	ic.mu.Lock()
   154  	defer ic.mu.Unlock()
   155  	return ic.mu.ta != nil && ic.mu.ta == ta
   156  }
   157  
   158  func (ic *InlineComplete) setOff(ta *ui.TextArea) {
   159  	ic.mu.Lock()
   160  	defer ic.mu.Unlock()
   161  	if ic.mu.ta == ta {
   162  		ic.mu.ta = nil
   163  		// possible early cancel for this textarea
   164  		ic.mu.cancel()
   165  	}
   166  }
   167  
   168  //----------
   169  
   170  func (ic *InlineComplete) CancelAndClear() {
   171  	ic.mu.Lock()
   172  	ta := ic.mu.ta
   173  	ic.mu.Unlock()
   174  	if ta != nil {
   175  		ic.setAnnotations(ta, nil)
   176  	}
   177  }
   178  
   179  func (ic *InlineComplete) CancelOnCursorChange() {
   180  	ic.mu.Lock()
   181  	ta := ic.mu.ta
   182  	index := ic.mu.index
   183  	ic.mu.Unlock()
   184  	if ta != nil {
   185  		if index != ta.CursorIndex() {
   186  			ic.setAnnotations(ta, nil)
   187  		}
   188  	}
   189  }
   190  
   191  //----------
   192  
   193  func insertComplete(comps []string, rw iorw.ReadWriterAt, index int) (newIndex int, completed bool, _ []string, _ error) {
   194  	// build prefix from start of string
   195  	start, prefix, ok := readLastUntilStart(rw, index)
   196  	if !ok {
   197  		return 0, false, comps, nil
   198  	}
   199  
   200  	expand, canComplete, comps2 := filterPrefixedAndExpand(comps, prefix)
   201  	comps = comps2
   202  	if len(comps) == 0 {
   203  		return 0, false, comps, nil
   204  	}
   205  
   206  	if canComplete {
   207  		// original string
   208  		origStr := prefix
   209  
   210  		// string to insert
   211  		n := len(origStr)
   212  		insStr := comps[0][:n+expand]
   213  
   214  		// try to expand the index to the existing text
   215  		for i := 0; i < expand; i++ {
   216  			b, err := rw.ReadFastAt(index+i, 1)
   217  			if err != nil {
   218  				break
   219  			}
   220  			if b[0] != insStr[n] {
   221  				break
   222  			}
   223  			n++
   224  		}
   225  
   226  		// insert completion
   227  		if insStr != origStr {
   228  			err := rw.OverwriteAt(start, n, []byte(insStr))
   229  			if err != nil {
   230  				return 0, false, nil, err
   231  			}
   232  			newIndex = start + len(insStr)
   233  			return newIndex, true, comps, nil
   234  		}
   235  	}
   236  
   237  	return 0, false, comps, nil
   238  }
   239  
   240  //----------
   241  
   242  func filterPrefixedAndExpand(comps []string, prefix string) (expand int, canComplete bool, _ []string) {
   243  	// find all matches from start to index
   244  	strLow := strings.ToLower(prefix)
   245  	res := []string{}
   246  	for _, v := range comps {
   247  		vLow := strings.ToLower(v)
   248  		if strings.HasPrefix(vLow, strLow) {
   249  			res = append(res, v)
   250  		}
   251  	}
   252  	// find possible expansions if all matches have common extra runes
   253  	if len(res) == 1 {
   254  		// special case to allow overwriting string casing "aaa"->"aAa"
   255  		canComplete = true
   256  		expand = len(res[0]) - len(prefix)
   257  	} else if len(res) >= 1 {
   258  	loop1:
   259  		for j := 0; j < len(res[0]); j++ { // test up to first result length
   260  			// break on any result that fails to expand
   261  			for i := 1; i < len(res); i++ {
   262  				if !(j < len(res[i]) && res[i][j] == res[0][j]) {
   263  					break loop1
   264  				}
   265  			}
   266  			if j >= len(prefix) {
   267  				expand++
   268  				canComplete = true
   269  			}
   270  		}
   271  	}
   272  
   273  	return expand, canComplete, res
   274  }
   275  
   276  //----------
   277  
   278  func readLastUntilStart(rd iorw.ReaderAt, index int) (int, string, bool) {
   279  	sc := parseutil.NewScannerR(rd, index)
   280  	sc.Reverse = true
   281  	pos0 := sc.KeepPos()
   282  	max := 1000
   283  	err := sc.M.RuneFnLoop(func(ru rune) bool {
   284  		max--
   285  		if max <= 0 {
   286  			return false
   287  		}
   288  		return ru == '_' ||
   289  			unicode.IsLetter(ru) ||
   290  			unicode.IsNumber(ru) ||
   291  			unicode.IsDigit(ru)
   292  	})
   293  	if err != nil || pos0.IsEmpty() {
   294  		return 0, "", false
   295  	}
   296  	return sc.Pos(), string(pos0.Bytes()), true
   297  }