From ff11be592253684c8dfe69bbfec8de0f1762f909 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 21 Nov 2022 12:55:53 -0800 Subject: [PATCH] checkpoint on more completion/expansion --- pkg/shparse/expand.go | 69 +++++++++++++++++++++--- pkg/shparse/shparse.go | 101 +++++++++++++++++++++++++++--------- pkg/shparse/shparse_test.go | 50 ++++++++---------- 3 files changed, 160 insertions(+), 60 deletions(-) diff --git a/pkg/shparse/expand.go b/pkg/shparse/expand.go index 2f8d8910..9f872311 100644 --- a/pkg/shparse/expand.go +++ b/pkg/shparse/expand.go @@ -145,12 +145,26 @@ func simpleExpandSubs(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, w } } +func canExpand(ectx ExpandContext, wtype string) bool { + return wtype == WordTypeLit || wtype == WordTypeSQ || wtype == WordTypeDSQ || + wtype == WordTypeDQ || wtype == WordTypeDDQ || wtype == WordTypeGroup +} + func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) { - if pos >= word.contentEndPos() { - pos = len(word.Raw) - } - if pos <= word.contentStartPos() { - return + if canExpand(ectx, word.Type) { + if pos >= word.contentEndPos() { + pos = word.contentEndPos() + } + if pos <= word.contentStartPos() { + return + } + } else { + if pos >= len(word.Raw) { + pos = len(word.Raw) + } + if pos <= 0 { + return + } } switch word.Type { @@ -177,16 +191,19 @@ func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, w simpleExpandSubs(buf, info, ectx, word, pos) return + // not expanded case WordTypeSimpleVar: + info.HasVar = true + buf.WriteString(string(word.Raw[:pos])) return + // not expanded case WordTypeVarBrace: + info.HasVar = true + buf.WriteString(string(word.Raw[:pos])) return default: - if pos > len(word.Raw) { - pos = len(word.Raw) - } info.HasSpecial = true buf.WriteString(string(word.Raw[:pos])) return @@ -203,3 +220,39 @@ func SimpleExpandPrefix(ectx ExpandContext, word *WordType, pos int) (string, Ex func SimpleExpand(ectx ExpandContext, word *WordType) (string, ExpandInfo) { return SimpleExpandPrefix(ectx, word, len(word.Raw)) } + +// returns varname (no '$') and ok (whether this is a valid varname expansion) +func SimpleVarNamePrefix(ectx ExpandContext, word *WordType, pos int) (string, bool) { + if word.Type != WordTypeSimpleVar && word.Type != WordTypeVarBrace { + return "", false + } + if word.Type == WordTypeSimpleVar { + if pos == 0 { + return "", false + } + if pos == 1 { + return "", true + } + if pos > len(word.Raw) { + pos = len(word.Raw) + } + return string(word.Raw[1:pos]), true + } + + // word.Type == WordTypeVarBrace + // knock '${' off the front, then see if the rest is a valid var name. + if pos == 0 || pos == 1 { + return "", false + } + if pos == 2 { + return "", true + } + if pos > word.contentEndPos() { + pos = word.contentEndPos() + } + rawVarName := word.Raw[2:pos] + if isSimpleVarName(rawVarName) { + return string(rawVarName), true + } + return "", false +} diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go index e0f49eb5..c3c23dc5 100644 --- a/pkg/shparse/shparse.go +++ b/pkg/shparse/shparse.go @@ -65,6 +65,15 @@ const ( CmdTypeSimple = "simple" // holds real commands ) +const ( + CompTypeCommand = "command" + CompTypeArg = "command-arg" + CompTypeInvalid = "invalid" + CompTypeVar = "var" + CompTypeAssignment = "assignment" + CompTypeBasic = "basic" +) + type WordType struct { Type string Offset int @@ -507,36 +516,52 @@ func identifyReservedWords(words []*WordType) { } type CompletionPos struct { - RawPos int // the raw position of cursor - SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset - Cmd *CmdType // nil if between commands (otherwise will be a SimpleCommand) + RawPos int // the raw position of cursor + SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset - // index into cmd.Words (only useful when Cmd is not nil, otherwise we look at CompCommand) + CompType string // see CompType* constants + Cmd *CmdType // nil if between commands or a special completion (otherwise will be a SimpleCommand) + // index into cmd.Words (only set when Cmd is not nil, otherwise we look at CompCommand) // 0 means command-word // negative means assignment-words. // can be past the end of Words (means start new word). - CmdWordPos int + 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) - CmdWord *WordType // set to the word we are completing (nil if we are starting a new word) - CmdWordOffset int // offset into cmdword (only if CmdWord is not nil) - CompInvalid bool // some words cannot be completed (e.g. in the middle of an operator, inside a control structure, etc.) - CompCommand bool // set when we think we are the first word of an existing or new command. otherwise we default to file completion +} + +func compTypeFromPos(cmdWordPos int) string { + if cmdWordPos == 0 { + return CompTypeCommand + } + if cmdWordPos < 0 { + return CompTypeAssignment + } + return CompTypeArg } func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) CompletionPos { if cmd.Type != CmdTypeSimple { panic("findCompletetionPos_simple only works for CmdTypeSimple") } + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd} for idx, word := range cmd.AssignmentWords { startOffset := word.Offset endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position (before the current assignment word) - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords) - 1} + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompType = CompTypeAssignment + return rtn } if pos <= endOffset { // completing an assignment word - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx - len(cmd.AssignmentWords), CmdWord: word, CmdWordOffset: pos - word.Offset} + rtn.CmdWordPos = idx - len(cmd.AssignmentWords) + rtn.CompWord = word + rtn.CompWordOffset = pos - word.Offset + rtn.CompType = CompTypeAssignment + return rtn } } var foundWord *WordType @@ -546,7 +571,9 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio endOffset := word.Offset + len(word.Raw) if pos <= startOffset { // starting a new word at this position - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: idx} + rtn.CmdWordPos = idx + rtn.CompType = compTypeFromPos(idx) + return rtn } if pos == endOffset && word.Type == WordTypeOp { // operators are special, they can allow a full-word completion at endpos @@ -559,14 +586,21 @@ func (cmd *CmdType) findCompletionPos_simple(pos int, superOffset int) Completio } } if foundWord != nil { + rtn.CmdWordPos = foundWordIdx + rtn.CompWord = foundWord + rtn.CompWordOffset = pos - foundWord.Offset if foundWord.uncompletable() { // invalid completion point - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset, CompInvalid: true} + rtn.CompType = CompTypeInvalid + return rtn } - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: foundWordIdx, CmdWord: foundWord, CmdWordOffset: pos - foundWord.Offset} + rtn.CompType = compTypeFromPos(foundWordIdx) + return rtn } // past the end, so we're starting a new word in Cmd - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: cmd, CmdWordPos: len(cmd.Words)} + rtn.CmdWordPos = len(cmd.Words) + rtn.CompType = CompTypeArg + return rtn } func (cmd *CmdType) findWordAtPos_none(pos int) *WordType { @@ -642,9 +676,11 @@ func findCompletionPosInWord(word *WordType, pos int, superOffset int) *Completi // if we are completing in a word, returns the Word. Word might be a group-word or DQ word, so it may need additional resolution (done in extend) // otherwise we are going to create a new word to insert at offset (so the context does not matter) func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) CompletionPos { + rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset} if len(cmds) == 0 { // set CompCommand because we're starting a new command - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } for _, cmd := range cmds { endOffset := cmd.endOffset() @@ -654,44 +690,59 @@ func findCompletionPosCmds(cmds []*CmdType, pos int, superOffset int) Completion startOffset := cmd.offset() if cmd.Type == CmdTypeSimple { if pos <= startOffset { - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } return cmd.findCompletionPos_simple(pos, superOffset) } else { // not in a simple-command // if we're before the none-command, just start a new command if pos <= startOffset { - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: true} + rtn.CompType = CompTypeCommand + return rtn } word := cmd.findWordAtPos_none(pos) if word == nil { // just revert to a file completion - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: false} + rtn.CompType = CompTypeBasic + return rtn } + rtn.CompWord = word + rtn.CompWordOffset = pos - word.Offset if word.uncompletable() { // ok, we're inside of a word in CmdTypeNone. if we're in an uncompletable word, return CompInvalid - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset, CompInvalid: true} + rtn.CompType = CompTypeInvalid + return rtn } // revert to file completion - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CmdWord: word, CmdWordOffset: pos - word.Offset} + rtn.CompType = CompTypeBasic + return rtn } } // past the end lastCmd := cmds[len(cmds)-1] if lastCmd.Type == CmdTypeSimple { // just extend last command - return CompletionPos{RawPos: pos, SuperOffset: superOffset, Cmd: lastCmd, CmdWordPos: len(lastCmd.Words)} + rtn.Cmd = lastCmd + rtn.CmdWordPos = len(lastCmd.Words) + rtn.CompType = CompTypeArg + return rtn } // use lastCmd.NoneComplete to see if last command ended on a "separator". use that to set CompCommand - return CompletionPos{RawPos: pos, SuperOffset: superOffset, CompCommand: lastCmd.NoneComplete} + if lastCmd.NoneComplete { + rtn.CompType = CompTypeCommand + } else { + rtn.CompType = CompTypeBasic + } + return rtn } func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos { cpos := findCompletionPosCmds(cmds, pos, superOffset) - if cpos.CmdWord == nil { + if cpos.CompWord == nil { return cpos } - subPos := findCompletionPosInWord(cpos.CmdWord, cpos.CmdWordOffset, superOffset+cpos.CmdWord.Offset) + subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset) if subPos == nil { return cpos } else { diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 030c2ae5..e4d7cc62 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -105,14 +105,17 @@ func TestCmd(t *testing.T) { testParseCommands(t, `x="foo $y" z=10 ls`) } -func testCompPos(t *testing.T, cmdStr string, hasCommand bool, cmdWordPos int, hasWord bool, compInvalid bool, compCommand bool) { +func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool) { cmdSP := utilfn.ParseToSP(cmdStr) words := Tokenize(cmdSP.Str) cmds := ParseCommands(words) cpos := FindCompletionPos(cmds, cmdSP.Pos, 0) - fmt.Printf("testCompPos [%d] %q => %v\n", cmdSP.Pos, cmdStr, cpos) - if cpos.CmdWord != nil { - fmt.Printf(" found-word: %d %s\n", cpos.CmdWordOffset, cpos.CmdWord.stringWithPos(cpos.CmdWordOffset)) + fmt.Printf("testCompPos [%d] %q => [%s] %v\n", cmdSP.Pos, cmdStr, cpos.CompType, cpos) + if cpos.CompType != compType { + t.Errorf("testCompPos %q => invalid comp-type %q, expected %q", cmdStr, cpos.CompType, compType) + } + if cpos.CompWord != nil { + fmt.Printf(" found-word: %d %s\n", cpos.CompWordOffset, cpos.CompWord.stringWithPos(cpos.CompWordOffset)) } if cpos.Cmd != nil { fmt.Printf(" found-cmd: ") @@ -126,36 +129,27 @@ func testCompPos(t *testing.T, cmdStr string, hasCommand bool, cmdWordPos int, h if (cpos.Cmd != nil) != hasCommand { t.Errorf("testCompPos %q => bad has-command exp:%v", cmdStr, hasCommand) } - if (cpos.CmdWord != nil) != hasWord { + if (cpos.CompWord != nil) != hasWord { t.Errorf("testCompPos %q => bad has-word exp:%v", cmdStr, hasWord) } if cpos.CmdWordPos != cmdWordPos { t.Errorf("testCompPos %q => bad cmd-word-pos got:%d exp:%d", cmdStr, cpos.CmdWordPos, cmdWordPos) } - if cpos.CompInvalid != compInvalid { - t.Errorf("testCompPos %q => bad comp-invalid exp:%v", cmdStr, compInvalid) - } - if cpos.CompCommand != compCommand { - t.Errorf("testCompPos %q => bad comp-command exp:%v", cmdStr, compCommand) - } } func TestCompPos(t *testing.T) { - testCompPos(t, "ls [*]foo", true, 1, false, false, false) - testCompPos(t, "ls foo [*];", true, 2, false, false, false) - testCompPos(t, "ls foo ;[*]", false, 0, false, false, true) - testCompPos(t, "ls foo >[*]> ./bar", true, 2, true, true, false) - testCompPos(t, "l[*]s", true, 0, true, false, false) - testCompPos(t, "ls[*]", true, 0, true, false, false) - testCompPos(t, "x=10 { (ls ./f[*] more); ls }", true, 1, true, false, false) - testCompPos(t, "for x in 1[*] 2 3; do ", false, 0, true, false, false) - testCompPos(t, "for[*] x in 1 2 3;", false, 0, true, true, false) - - testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", true, 2, true, false, false) - - testCompPos(t, "ls ${abc:$(ls -l [*])}", true, 1, true, false, false) - - testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, true, 1, true, false, false) + testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false) + testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false) + testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false) + testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true) + testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true) + testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true) + testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true) + testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true) + testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true) + testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", CompTypeArg, true, 2, true) + testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeArg, true, 1, true) // we don't sub-parse inside of ${} + testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true) } func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) { @@ -182,9 +176,11 @@ func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *Expan func TestExpand(t *testing.T) { testExpand(t, "hello", 3, "hel", nil) testExpand(t, "he\\$xabc", 6, "he$xa", nil) - testExpand(t, "he${x}abc", 6, "he$xa", nil) + testExpand(t, "he${x}abc", 6, "he${x}", nil) testExpand(t, "'hello\"mike'", 8, "hello\"m", nil) testExpand(t, `$'abc\x01def`, 10, "abc\x01d", nil) testExpand(t, `$((2 + 2))`, 6, "$((2 +", &ExpandInfo{HasSpecial: true}) testExpand(t, `abc"def"`, 6, "abcde", nil) + testExpand(t, `"abc$x$'"'""`, 12, "abc$x\"", nil) + testExpand(t, `'he'\''s'`, 9, "he's", nil) }