Skip to content

Commit

Permalink
spell and complete code updated
Browse files Browse the repository at this point in the history
  • Loading branch information
rcoreilly committed Feb 19, 2025
1 parent dfafae2 commit ebf378c
Show file tree
Hide file tree
Showing 7 changed files with 692 additions and 352 deletions.
100 changes: 100 additions & 0 deletions text/lines/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,106 @@ func (ls *Lines) MoveLineEnd(vid int, pos textpos.Pos) textpos.Pos {
return ls.moveLineEnd(vw, pos)
}

//////// Words

// IsWordEnd returns true if the cursor is just past the last letter of a word.
func (ls *Lines) IsWordEnd(pos textpos.Pos) bool {
ls.Lock()
defer ls.Unlock()
if errors.Log(ls.isValidPos(pos)) != nil {
return false
}
txt := ls.lines[pos.Line]
sz := len(txt)
if sz == 0 {
return false
}
if pos.Char >= len(txt) { // end of line
r := txt[len(txt)-1]
return textpos.IsWordBreak(r, -1)
}
if pos.Char == 0 { // start of line
r := txt[0]
return !textpos.IsWordBreak(r, -1)
}
r1 := txt[pos.Char-1]
r2 := txt[pos.Char]
return !textpos.IsWordBreak(r1, rune(-1)) && textpos.IsWordBreak(r2, rune(-1))
}

// IsWordMiddle returns true if the cursor is anywhere inside a word,
// i.e. the character before the cursor and the one after the cursor
// are not classified as word break characters
func (ls *Lines) IsWordMiddle(pos textpos.Pos) bool {
ls.Lock()
defer ls.Unlock()
if errors.Log(ls.isValidPos(pos)) != nil {
return false
}
txt := ls.lines[pos.Line]
sz := len(txt)
if sz < 2 {
return false
}
if pos.Char >= len(txt) { // end of line
return false
}
if pos.Char == 0 { // start of line
return false
}
r1 := txt[pos.Char-1]
r2 := txt[pos.Char]
return !textpos.IsWordBreak(r1, rune(-1)) && !textpos.IsWordBreak(r2, rune(-1))
}

// WordAt returns a Region for a word starting at given position.
// If the current position is a word break then go to next
// break after the first non-break.
func (ls *Lines) WordAt(pos textpos.Pos) textpos.Region {
ls.Lock()
defer ls.Unlock()
if errors.Log(ls.isValidPos(pos)) != nil {
return textpos.Region{}
}
txt := ls.lines[pos.Line]
rng := textpos.WordAt(txt, pos.Char)
st := pos
st.Char = rng.Start
ed := pos
ed.Char = rng.End
return textpos.NewRegionPos(st, ed)
}

// WordBefore returns the word before the given source position.
// uses IsWordBreak to determine the bounds of the word
func (ls *Lines) WordBefore(pos textpos.Pos) *textpos.Edit {
ls.Lock()
defer ls.Unlock()
if errors.Log(ls.isValidPos(pos)) != nil {
return &textpos.Edit{}
}
txt := ls.lines[pos.Line]
ch := pos.Char
ch = min(ch, len(txt))
st := ch
for i := ch - 1; i >= 0; i-- {
if i == 0 { // start of line
st = 0
break
}
r1 := txt[i]
r2 := txt[i-1]
if textpos.IsWordBreak(r1, r2) {
st = i + 1
break
}
}
if st != ch {
return ls.region(textpos.Pos{Line: pos.Line, Char: st}, pos)
}
return nil
}

//////// PosHistory

// PosHistorySave saves the cursor position in history stack of cursor positions.
Expand Down
4 changes: 2 additions & 2 deletions text/spell/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func CheckLexLine(src []rune, tags lexer.Line) lexer.Line {
t.Token.Token = token.TextSpellErr
widx := strings.Index(wrd, lwrd)
ld := len(wrd) - len(lwrd)
t.St += widx
t.Ed += widx - ld
t.Start += widx
t.End += widx - ld
t.Now()
ser = append(ser, t)
}
Expand Down
158 changes: 158 additions & 0 deletions text/textcore/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,172 @@ package textcore

import (
"fmt"
"strings"

"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/text/lines"
"cogentcore.org/core/text/parse"
"cogentcore.org/core/text/parse/complete"
"cogentcore.org/core/text/parse/parser"
"cogentcore.org/core/text/textpos"
)

// setCompleter sets completion functions so that completions will
// automatically be offered as the user types
func (ed *Editor) setCompleter(data any, matchFun complete.MatchFunc, editFun complete.EditFunc,
lookupFun complete.LookupFunc) {
if ed.Complete != nil {
if ed.Complete.Context == data {
ed.Complete.MatchFunc = matchFun
ed.Complete.EditFunc = editFun
ed.Complete.LookupFunc = lookupFun
return
}
ed.deleteCompleter()
}
ed.Complete = core.NewComplete().SetContext(data).SetMatchFunc(matchFun).
SetEditFunc(editFun).SetLookupFunc(lookupFun)
ed.Complete.OnSelect(func(e events.Event) {
ed.completeText(ed.Complete.Completion)
})
// todo: what about CompleteExtend event type?
// TODO(kai/complete): clean this up and figure out what to do about Extend and only connecting once
// note: only need to connect once..
// tb.Complete.CompleteSig.ConnectOnly(func(dlg *core.Dialog) {
// tbf, _ := recv.Embed(TypeBuf).(*Buf)
// if sig == int64(core.CompleteSelect) {
// tbf.CompleteText(data.(string)) // always use data
// } else if sig == int64(core.CompleteExtend) {
// tbf.CompleteExtend(data.(string)) // always use data
// }
// })
}

func (ed *Editor) deleteCompleter() {
if ed.Complete == nil {
return
}
ed.Complete = nil
}

// completeText edits the text using the string chosen from the completion menu
func (ed *Editor) completeText(s string) {
if s == "" {
return
}
// give the completer a chance to edit the completion before insert,
// also it return a number of runes past the cursor to delete
st := textpos.Pos{ed.Complete.SrcLn, 0}
en := textpos.Pos{ed.Complete.SrcLn, ed.Lines.LineLen(ed.Complete.SrcLn)}
var tbes string
tbe := ed.Lines.Region(st, en)
if tbe != nil {
tbes = string(tbe.ToBytes())
}
c := ed.Complete.GetCompletion(s)
pos := textpos.Pos{ed.Complete.SrcLn, ed.Complete.SrcCh}
ced := ed.Complete.EditFunc(ed.Complete.Context, tbes, ed.Complete.SrcCh, c, ed.Complete.Seed)
if ced.ForwardDelete > 0 {
delEn := textpos.Pos{ed.Complete.SrcLn, ed.Complete.SrcCh + ced.ForwardDelete}
ed.Lines.DeleteText(pos, delEn)
}
// now the normal completion insertion
st = pos
st.Char -= len(ed.Complete.Seed)
ed.Lines.ReplaceText(st, pos, st, ced.NewText, lines.ReplaceNoMatchCase)
ep := st
ep.Char += len(ced.NewText) + ced.CursorAdjust
ed.SetCursorShow(ep)
}

// offerComplete pops up a menu of possible completions
func (ed *Editor) offerComplete() {
if ed.Complete == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() {
return
}
ed.Complete.Cancel()
if !ed.Lines.Settings.Completion {
return
}
if ed.Lines.InComment(ed.CursorPos) || ed.Lines.InLitString(ed.CursorPos) {
return
}

ed.Complete.SrcLn = ed.CursorPos.Line
ed.Complete.SrcCh = ed.CursorPos.Char
st := textpos.Pos{ed.CursorPos.Line, 0}
en := textpos.Pos{ed.CursorPos.Line, ed.CursorPos.Char}
tbe := ed.Lines.Region(st, en)
var s string
if tbe != nil {
s = string(tbe.ToBytes())
s = strings.TrimLeft(s, " \t") // trim ' ' and '\t'
}

// count := ed.Buf.ByteOffs[ed.CursorPos.Line] + ed.CursorPos.Char
cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location
cpos.X += 5
cpos.Y += 10
ed.Complete.SrcLn = ed.CursorPos.Line
ed.Complete.SrcCh = ed.CursorPos.Char
ed.Complete.Show(ed, cpos, s)
}

// CancelComplete cancels any pending completion.
// Call this when new events have moved beyond any prior completion scenario.
func (ed *Editor) CancelComplete() {
if ed.Lines == nil {
return
}
if ed.Complete == nil {
return
}
ed.Complete.Cancel()
}

// Lookup attempts to lookup symbol at current location, popping up a window
// if something is found.
func (ed *Editor) Lookup() { //types:add
if ed.Complete == nil || ed.ISearch.On || ed.QReplace.On || ed.IsDisabled() {
return
}

var ln int
var ch int
if ed.HasSelection() {
ln = ed.SelectRegion.Start.Line
if ed.SelectRegion.End.Line != ln {
return // no multiline selections for lookup
}
ch = ed.SelectRegion.End.Char
} else {
ln = ed.CursorPos.Line
if ed.Lines.IsWordEnd(ed.CursorPos) {
ch = ed.CursorPos.Char
} else {
ch = ed.Lines.WordAt(ed.CursorPos).End.Char
}
}
ed.Complete.SrcLn = ln
ed.Complete.SrcCh = ch
st := textpos.Pos{ed.CursorPos.Line, 0}
en := textpos.Pos{ed.CursorPos.Line, ch}

tbe := ed.Lines.Region(st, en)
var s string
if tbe != nil {
s = string(tbe.ToBytes())
s = strings.TrimLeft(s, " \t") // trim ' ' and '\t'
}

// count := ed.Buf.ByteOffs[ed.CursorPos.Line] + ed.CursorPos.Char
cpos := ed.charStartPos(ed.CursorPos).ToPoint() // physical location
cpos.X += 5
cpos.Y += 10
ed.Complete.Lookup(s, ed.CursorPos.Line, ed.CursorPos.Char, ed.Scene, cpos)
}

// completeParse uses [parse] symbols and language; the string is a line of text
// up to point where user has typed.
// The data must be the *FileState from which the language type is obtained.
Expand Down
6 changes: 6 additions & 0 deletions text/textcore/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ type Editor struct { //core:embedder

// QReplace is the query replace data.
QReplace QReplace `set:"-" edit:"-" json:"-" xml:"-"`

// Complete is the functions and data for text completion.
Complete *core.Complete `json:"-" xml:"-"`

// spell is the functions and data for spelling correction.
spell *spellCheck
}

func (ed *Editor) Init() {
Expand Down
Loading

0 comments on commit ebf378c

Please sign in to comment.