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 }