diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index 176378af..136bf365 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -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 diff --git a/pkg/shparse/extend.go b/pkg/shparse/extend.go index 5761d0bd..f7bbd8f0 100644 --- a/pkg/shparse/extend.go +++ b/pkg/shparse/extend.go @@ -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 } diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index ac09762f..ecf9b947 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -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 } diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 371791cd..1740e2dc 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -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) {