checkpoint -- extension

This commit is contained in:
sawka 2022-11-21 19:06:59 -08:00
parent 8729d1f491
commit 75f662a188
4 changed files with 231 additions and 131 deletions

View File

@ -1,12 +1,13 @@
package shparse
const (
CompTypeCommand = "command"
CompTypeArg = "command-arg"
CompTypeInvalid = "invalid"
CompTypeVar = "var"
CompTypeAssignment = "assignment"
CompTypeBasic = "basic"
CompTypeCommandMeta = "command-meta"
CompTypeCommand = "command"
CompTypeArg = "command-arg"
CompTypeInvalid = "invalid"
CompTypeVar = "var"
CompTypeAssignment = "assignment"
CompTypeBasic = "basic"
)
type CompletionPos struct {
@ -22,7 +23,6 @@ type CompletionPos struct {
CmdWordPos int
CompWord *WordType // set to the word we are completing (nil if we are starting a new word)
CompWordOffset int // offset into compword (only if CmdWord is not nil)
}
func compTypeFromPos(cmdWordPos int) string {
@ -97,10 +97,10 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio
return rtn
}
func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos {
func (cmd *CmdType) findCompletionPos_none(pos int, superOffset int) CompletionPos {
rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset}
if cmd.Type != CmdTypeNone {
panic("findCompletionWordAtPos_none only works for CmdTypeNone")
panic("findCompletionPos_none only works for CmdTypeNone")
}
var foundWord *WordType
for _, word := range cmd.Words {
@ -123,25 +123,31 @@ func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) Compl
rtn.CompType = CompTypeBasic
return rtn
}
foundWordOffset := pos - foundWord.Offset
rtn.CompWord = foundWord
rtn.CompWordOffset = pos - foundWord.Offset
rtn.CompWordOffset = foundWordOffset
if foundWord.uncompletable() {
// ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid
rtn.CompType = CompTypeInvalid
return rtn
}
if foundWordOffset > 0 && foundWordOffset < foundWord.contentStartPos() {
// cursor is in a weird position, between characters of a multi-char prefix (e.g. "$[*]{hello}" or $[*]'hello'). cannot complete.
rtn.CompType = CompTypeInvalid
return rtn
}
// revert to file completion
rtn.CompType = CompTypeBasic
return rtn
}
func findCompletionWordAtPos(words []*WordType, pos int) *WordType {
// WordTypeSimpleVar is special, if cursor is at the end of SimpleVar it is returned
func findCompletionWordAtPos(words []*WordType, pos int, allowEndMatch bool) *WordType {
// WordTypeSimpleVar is special (always allowEndMatch), if cursor is at the end of SimpleVar it is returned
for _, word := range words {
if pos > word.Offset && pos < word.Offset+len(word.Raw) {
return word
}
if word.Type == WordTypeSimpleVar && pos == word.Offset+len(word.Raw) {
if (allowEndMatch || word.Type == WordTypeSimpleVar) && pos == word.Offset+len(word.Raw) {
return word
}
}
@ -159,7 +165,7 @@ func findCompletionPosInWord(word *WordType, pos int, superOffset int) *Completi
if pos > word.contentEndPos() {
return nil
}
subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos())
subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos(), false)
if subWord == nil {
return nil
}
@ -218,7 +224,7 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion
rtn.CompType = CompTypeCommand
return rtn
}
return cmd.findCompletionWordAtPos_none(pos, superOffset)
return cmd.findCompletionPos_none(pos, superOffset)
}
}
// past the end

View File

@ -93,35 +93,108 @@ func (ec *extendContext) appendWord(w *WordType) {
func (ec *extendContext) ensureCurWord() {
if ec.CurWord == nil || ec.CurWord.Type != ec.Intention {
ec.CurWord = MakeEmptyWord(ec.Intention, ec.QC, 0)
ec.CurWord = MakeEmptyWord(ec.Intention, ec.QC, 0, true)
ec.Rtn = append(ec.Rtn, ec.CurWord)
}
}
// grp, dq, ddq
func extendWithSubs(buf *bytes.Buffer, word *WordType, wordPos int, extStr string) {
}
// lit, svar, varb, sq, dsq
func extendLeafCh(buf *bytes.Buffer, wordOpen *bool, wtype string, qc QuoteContext, ch rune) {
switch wtype {
case WordTypeSimpleVar, WordTypeVarBrace:
extendVar(buf, ch)
case WordTypeLit:
if qc.cur() == WordTypeDQ {
extendDQLit(buf, wordOpen, ch)
} else {
extendLit(buf, ch)
}
case WordTypeSQ:
extendSQ(buf, wordOpen, ch)
case WordTypeDSQ:
extendDSQ(buf, wordOpen, ch)
default:
return
}
}
// lit, svar, varb sq, dsq
func extendLeaf(buf *bytes.Buffer, wordOpen *bool, word *WordType, wordPos int, extStr string) {
for _, ch := range extStr {
extendLeafCh(buf, wordOpen, word.Type, word.QC, ch)
}
}
// lit, grp, svar, dq, ddq, varb, sq, dsq
func Extend(word *WordType, wordPos int, extStr string, complete bool) utilfn.StrWithPos {
if extStr == "" {
return utilfn.StrWithPos{Str: string(word.Raw), Pos: wordPos}
}
var buf bytes.Buffer
isEOW := wordPos >= word.contentEndPos()
if isEOW {
wordPos = word.contentEndPos()
}
if wordPos > 0 && wordPos < word.contentStartPos() {
wordPos = word.contentStartPos()
}
wordOpen := false
if wordPos >= word.contentStartPos() {
wordOpen = true
}
buf.WriteString(string(word.Raw[0:wordPos])) // write the prefix
if word.canHaveSubs() {
extendWithSubs(&buf, word, wordPos, extStr)
} else {
extendLeaf(&buf, &wordOpen, word, wordPos, extStr)
}
if isEOW {
// end-of-word, write the suffix (and optional ' '). return the end of the string
wmeta := wordMetaMap[word.Type]
buf.WriteString(wmeta.getSuffix())
var rtnPos int
if complete {
buf.WriteRune(' ')
rtnPos = utf8.RuneCount(buf.Bytes())
} else {
rtnPos = utf8.RuneCount(buf.Bytes()) - wmeta.SuffixLen
}
return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos}
}
// completion in the middle of a word (no ' ')
rtnPos := utf8.RuneCount(buf.Bytes())
buf.WriteString(string(word.Raw[wordPos:])) // write the suffix
return utilfn.StrWithPos{Str: buf.String(), Pos: rtnPos}
}
func (ec *extendContext) extend(ch rune) {
if ch == 0 {
return
}
switch ec.Intention {
return
}
case WordTypeSimpleVar, WordTypeVarBrace:
ec.extendVar(ch)
func isVarNameChar(ch rune) bool {
return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
}
case WordTypeDQ, WordTypeDDQ:
ec.extendDQ(ch)
case WordTypeSQ:
ec.extendSQ(ch)
case WordTypeDSQ:
ec.extendDSQ(ch)
case WordTypeLit:
ec.extendLit(ch)
default:
func extendVar(buf *bytes.Buffer, ch rune) {
if ch == 0 {
return
}
if !isVarNameChar(ch) {
return
}
buf.WriteRune(ch)
}
func getSpecialEscape(ch rune) string {
@ -131,122 +204,110 @@ func getSpecialEscape(ch rune) string {
return specialEsc[byte(ch)]
}
func isVarNameChar(ch rune) bool {
return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
func writeSpecial(buf *bytes.Buffer, ch rune) {
sesc := getSpecialEscape(ch)
if sesc != "" {
buf.WriteString(sesc)
} else {
utf8Lit := getUtf8Literal(ch)
buf.WriteString(utf8Lit)
}
}
func (ec *extendContext) extendVar(ch rune) {
if ch == 0 {
return
}
if !isVarNameChar(ch) {
return
}
ec.ensureCurWord()
ec.CurWord.writeRune(ch)
}
func (ec *extendContext) extendLit(ch rune) {
func extendLit(buf *bytes.Buffer, ch rune) {
if ch == 0 {
return
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0)
ec.appendWord(dsqWord)
sesc := getSpecialEscape(ch)
if sesc != "" {
dsqWord.writeString(sesc)
return
} else {
utf8Lit := getUtf8Literal(ch)
dsqWord.writeString(utf8Lit)
}
writeSpecial(buf, ch)
return
}
var bch = byte(ch)
ec.ensureCurWord()
if noEscChars[bch] {
ec.CurWord.writeRune(ch)
buf.WriteRune(ch)
return
}
ec.CurWord.writeRune('\\')
ec.CurWord.writeRune(ch)
buf.WriteRune('\\')
buf.WriteRune(ch)
return
}
func (ec *extendContext) extendDSQ(ch rune) {
func extendDSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) {
if ch == 0 {
return
}
ec.ensureCurWord()
if ch == '\'' {
ec.CurWord.writeRune('\\')
ec.CurWord.writeRune(ch)
return
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
sesc := getSpecialEscape(ch)
if sesc != "" {
ec.CurWord.writeString(sesc)
} else {
utf8Lit := getUtf8Literal(ch)
ec.CurWord.writeString(utf8Lit)
if *wordOpen {
buf.WriteRune('\'')
*wordOpen = false
}
writeSpecial(buf, ch)
return
}
ec.CurWord.writeRune(ch)
if *wordOpen {
buf.WriteRune('$')
buf.WriteRune('\'')
*wordOpen = true
}
if ch == '\'' {
buf.WriteRune('\\')
buf.WriteRune(ch)
return
}
buf.WriteRune(ch)
return
}
func (ec *extendContext) extendSQ(ch rune) {
func extendSQ(buf *bytes.Buffer, wordOpen *bool, ch rune) {
if ch == 0 {
return
}
if ch == '\'' {
litWord := &WordType{Type: WordTypeLit, QC: ec.QC}
litWord.Raw = []rune{'\\', '\''}
ec.appendWord(litWord)
if *wordOpen {
buf.WriteRune('\'')
*wordOpen = false
}
buf.WriteRune('\\')
buf.WriteRune('\'')
return
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0)
ec.appendWord(dsqWord)
sesc := getSpecialEscape(ch)
if sesc != "" {
dsqWord.writeString(sesc)
} else {
utf8Lit := getUtf8Literal(ch)
dsqWord.writeString(utf8Lit)
if *wordOpen {
buf.WriteRune('\'')
*wordOpen = false
}
writeSpecial(buf, ch)
return
}
ec.ensureCurWord()
ec.CurWord.writeRune(ch)
if !*wordOpen {
buf.WriteRune('\'')
*wordOpen = true
}
buf.WriteRune(ch)
return
}
func (ec *extendContext) extendDQ(ch rune) {
func extendDQLit(buf *bytes.Buffer, wordOpen *bool, ch rune) {
if ch == 0 {
return
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
if *wordOpen {
buf.WriteRune('"')
*wordOpen = false
}
writeSpecial(buf, ch)
return
}
if !*wordOpen {
buf.WriteRune('"')
*wordOpen = true
}
if ch == '"' || ch == '\\' || ch == '$' || ch == '`' {
ec.ensureCurWord()
ec.CurWord.writeRune('\\')
ec.CurWord.writeRune(ch)
buf.WriteRune('\\')
buf.WriteRune(ch)
return
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
dsqWord := MakeEmptyWord(WordTypeDSQ, ec.QC, 0)
ec.appendWord(dsqWord)
sesc := getSpecialEscape(ch)
if sesc != "" {
dsqWord.writeString(sesc)
} else {
utf8Lit := getUtf8Literal(ch)
dsqWord.writeString(utf8Lit)
}
return
}
ec.CurWord.writeRune(ch)
buf.WriteRune(ch)
return
}

View File

@ -44,7 +44,7 @@ const (
WordTypeLit = "lit" // (can-extend)
WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing)
WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[
WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs)
WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x (has-subs) (can-extend)
WordTypeSimpleVar = "svar" // simplevar $ (can-extend)
WordTypeDQ = "dq" // " (quote-context) (can-extend) (has-subs)
@ -113,6 +113,20 @@ type wordMeta struct {
QuoteContext bool
}
func (m wordMeta) getSuffix() string {
if m.SuffixLen == 0 {
return ""
}
return string(m.EmptyWord[len(m.EmptyWord)-m.SuffixLen:])
}
func (m wordMeta) getPrefix() string {
if m.PrefixLen == 0 {
return ""
}
return string(m.EmptyWord[:m.PrefixLen])
}
func makeWordMeta(wtype string, emptyWord string, prefixLen int, suffixLen int, canExtend bool, quoteContext bool) {
if len(emptyWord) != prefixLen+suffixLen {
panic(fmt.Sprintf("invalid empty word %s %d %d", emptyWord, prefixLen, suffixLen))
@ -140,14 +154,18 @@ func init() {
makeWordMeta(WordTypeDB, "$[]", 2, 1, false, false)
}
func MakeEmptyWord(wtype string, qc QuoteContext, offset int) *WordType {
func MakeEmptyWord(wtype string, qc QuoteContext, offset int, complete bool) *WordType {
meta := wordMetaMap[wtype]
if meta.Type == "" {
meta = wordMetaMap[WordTypeRaw]
}
rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: true}
rtn := &WordType{Type: meta.Type, QC: qc, Offset: offset, Complete: complete}
if len(meta.EmptyWord) > 0 {
rtn.Raw = append([]rune(nil), meta.EmptyWord...)
if complete {
rtn.Raw = append([]rune(nil), meta.EmptyWord...)
} else {
rtn.Raw = append([]rune(nil), []rune(meta.getPrefix())...)
}
}
return rtn
}

View File

@ -57,33 +57,46 @@ func lastWord(words []*WordType) *WordType {
return words[len(words)-1]
}
func testExtend(t *testing.T, startStr string, extendStr string, expectedStr string) {
words := Tokenize(startStr)
ec := makeExtendContext(nil, lastWord(words))
for _, ch := range extendStr {
ec.extend(ch)
func testExtend(t *testing.T, startStr string, extendStr string, complete bool, expStr string) {
startSP := utilfn.ParseToSP(startStr)
words := Tokenize(startSP.Str)
word := findCompletionWordAtPos(words, startSP.Pos, true)
if word == nil {
word = MakeEmptyWord(WordTypeLit, nil, startSP.Pos, true)
}
ec.ensureCurWord()
output := wordsToStr(ec.Rtn)
fmt.Printf("[%s] + [%s] => [%s]\n", startStr, extendStr, output)
if output != expectedStr {
t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, output, expectedStr)
outSP := Extend(word, startSP.Pos-word.Offset, extendStr, complete)
expSP := utilfn.ParseToSP(expStr)
fmt.Printf("extend: [%s] + [%s] => [%s]\n", startStr, extendStr, outSP)
if outSP != expSP {
t.Errorf("extension does not match: [%s] + [%s] => [%s] expected [%s]\n", startStr, extendStr, outSP, expSP)
}
}
func Test2(t *testing.T) {
testExtend(t, `'he'`, "llo", `'hello'`)
testExtend(t, `'he'`, "'", `'he'\'''`)
testExtend(t, `'he'`, "'\x01", `'he'\'$'\x01'''`)
testExtend(t, `he`, "llo", `hello`)
testExtend(t, `he`, "l*l'\x01\x07o", `hel\*l\'$'\x01'$'\a'o`)
testExtend(t, `$x`, "fo|o", `$xfoo`)
testExtend(t, `${x`, "fo|o", `${xfoo`)
testExtend(t, `$'f`, "oo", `$'foo`)
testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`)
testExtend(t, `"f"`, "oo", `"foo"`)
testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`)
testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\a'""`)
testExtend(t, `he[*]`, "llo", false, "hello[*]")
testExtend(t, `he[*]`, "llo", true, "hello [*]")
testExtend(t, `'mi[*]e`, "k", false, "'mik[*]e")
testExtend(t, `'mi[*]e`, "k", true, "'mik[*]e")
testExtend(t, `'mi[*]'`, "ke", true, "'mike' [*]")
testExtend(t, `'mi'[*]`, "ke", true, "'mike' [*]")
testExtend(t, `'mi[*]'`, "ke", false, "'mike[*]'")
testExtend(t, `'mi'[*]`, "ke", false, "'mike[*]'")
testExtend(t, `$f[*]`, "oo", false, "$foo[*]")
testExtend(t, `${f}[*]`, "oo", false, "${foo[*]}")
testExtend(t, `${f[*]}`, "oo", true, "${foo} [*]")
// testExtend(t, `'he'`, "llo", `'hello'`)
// testExtend(t, `'he'`, "'", `'he'\'''`)
// testExtend(t, `'he'`, "'\x01", `'he'\'$'\x01'''`)
// testExtend(t, `he`, "llo", `hello`)
// testExtend(t, `he`, "l*l'\x01\x07o", `hel\*l\'$'\x01'$'\a'o`)
// testExtend(t, `$x`, "fo|o", `$xfoo`)
// testExtend(t, `${x`, "fo|o", `${xfoo`)
// testExtend(t, `$'f`, "oo", `$'foo`)
// testExtend(t, `$'f`, "'\x01\x07o", `$'f\'\x01\ao`)
// testExtend(t, `"f"`, "oo", `"foo"`)
// testExtend(t, `"mi"`, "ke's \"hello\"", `"mike's \"hello\""`)
// testExtend(t, `"t"`, "t\x01\x07", `"tt"$'\x01'$'\a'""`)
}
func testParseCommands(t *testing.T, str string) {
@ -151,6 +164,8 @@ func TestCompPos(t *testing.T) {
testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true) // we don't sub-parse inside of ${} (so this returns "var" right now)
testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true)
testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true)
testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true)
testCompPos(t, `ls $[*]'foo`, CompTypeInvalid, false, 0, false)
}
func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) {