From 9f7b5c82266872054695116b48cad3784419d933 Mon Sep 17 00:00:00 2001
From: sawka <mike.sawka@gmail.com>
Date: Mon, 21 Nov 2022 14:47:00 -0800
Subject: [PATCH] checkpoint, return comptype 'var' when we're in a var word

---
 pkg/shparse/comp.go         | 253 ++++++++++++++++++++++++++++++++++++
 pkg/shparse/shparse.go      | 246 -----------------------------------
 pkg/shparse/shparse_test.go |   3 +-
 3 files changed, 255 insertions(+), 247 deletions(-)
 create mode 100644 pkg/shparse/comp.go

diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go
new file mode 100644
index 000000000..176378af4
--- /dev/null
+++ b/pkg/shparse/comp.go
@@ -0,0 +1,253 @@
+package shparse
+
+const (
+	CompTypeCommand    = "command"
+	CompTypeArg        = "command-arg"
+	CompTypeInvalid    = "invalid"
+	CompTypeVar        = "var"
+	CompTypeAssignment = "assignment"
+	CompTypeBasic      = "basic"
+)
+
+type CompletionPos struct {
+	RawPos      int // the raw position of cursor
+	SuperOffset int // adjust all offsets in Cmd and CmdWord by SuperOffset
+
+	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
+	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 {
+	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)
+			rtn.CmdWordPos = idx - len(cmd.AssignmentWords)
+			rtn.CompType = CompTypeAssignment
+			return rtn
+		}
+		if pos <= endOffset {
+			// completing an assignment word
+			rtn.CmdWordPos = idx - len(cmd.AssignmentWords)
+			rtn.CompWord = word
+			rtn.CompWordOffset = pos - word.Offset
+			rtn.CompType = CompTypeAssignment
+			return rtn
+		}
+	}
+	var foundWord *WordType
+	var foundWordIdx int
+	for idx, word := range cmd.Words {
+		startOffset := word.Offset
+		endOffset := word.Offset + len(word.Raw)
+		if pos <= startOffset {
+			// starting a new word at this position
+			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
+			continue
+		}
+		if pos <= endOffset {
+			foundWord = word
+			foundWordIdx = idx
+			break
+		}
+	}
+	if foundWord != nil {
+		rtn.CmdWordPos = foundWordIdx
+		rtn.CompWord = foundWord
+		rtn.CompWordOffset = pos - foundWord.Offset
+		if foundWord.uncompletable() {
+			// invalid completion point
+			rtn.CompType = CompTypeInvalid
+			return rtn
+		}
+		rtn.CompType = compTypeFromPos(foundWordIdx)
+		return rtn
+	}
+	// past the end, so we're starting a new word in Cmd
+	rtn.CmdWordPos = len(cmd.Words)
+	rtn.CompType = CompTypeArg
+	return rtn
+}
+
+func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos {
+	rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset}
+	if cmd.Type != CmdTypeNone {
+		panic("findCompletionWordAtPos_none only works for CmdTypeNone")
+	}
+	var foundWord *WordType
+	for _, word := range cmd.Words {
+		startOffset := word.Offset
+		endOffset := word.Offset + len(word.Raw)
+		if pos <= startOffset {
+			break
+		}
+		if pos <= endOffset {
+			if pos == endOffset && word.Type == WordTypeOp {
+				// operators are special, they can allow a full-word completion at endpos
+				continue
+			}
+			foundWord = word
+			break
+		}
+	}
+	if foundWord == nil {
+		// just revert to a file completion
+		rtn.CompType = CompTypeBasic
+		return rtn
+	}
+	rtn.CompWord = foundWord
+	rtn.CompWordOffset = pos - foundWord.Offset
+	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
+	}
+	// 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
+	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) {
+			return word
+		}
+	}
+	return nil
+}
+
+// recursively descend down the word, parse commands and find a sub completion point if any.
+// return nil if there is no sub completion point in this word
+func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos {
+	if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ {
+		// need to descend further
+		if pos <= word.contentStartPos() {
+			return nil
+		}
+		if pos > word.contentEndPos() {
+			return nil
+		}
+		subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos())
+		if subWord == nil {
+			return nil
+		}
+		fullOffset := subWord.Offset + word.contentStartPos()
+		return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset)
+	}
+	if word.Type == WordTypeDP || word.Type == WordTypeBQ {
+		if pos < word.contentStartPos() {
+			return nil
+		}
+		if pos > word.contentEndPos() {
+			return nil
+		}
+		subCmds := ParseCommands(word.Subs)
+		newPos := FindCompletionPos(subCmds, pos-word.contentStartPos(), superOffset+word.contentStartPos())
+		return &newPos
+	}
+	if word.Type == WordTypeSimpleVar || word.Type == WordTypeVarBrace {
+		// special "var" completion
+		rtn := &CompletionPos{RawPos: pos, SuperOffset: superOffset}
+		rtn.CompType = CompTypeVar
+		rtn.CompWordOffset = pos
+		rtn.CompWord = word
+		return rtn
+	}
+	return nil
+}
+
+// returns the context for completion
+// if we are completing in a simple-command, the returns the Cmd.  the Cmd can be used for specialized completion (command name, arg position, etc.)
+// 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
+		rtn.CompType = CompTypeCommand
+		return rtn
+	}
+	for _, cmd := range cmds {
+		endOffset := cmd.endOffset()
+		if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) {
+			continue
+		}
+		startOffset := cmd.offset()
+		if cmd.Type == CmdTypeSimple {
+			if pos <= startOffset {
+				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 {
+				rtn.CompType = CompTypeCommand
+				return rtn
+			}
+			return cmd.findCompletionWordAtPos_none(pos, superOffset)
+		}
+	}
+	// past the end
+	lastCmd := cmds[len(cmds)-1]
+	if lastCmd.Type == CmdTypeSimple {
+		// just extend last command
+		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
+	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.CompWord == nil {
+		return cpos
+	}
+	subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset)
+	if subPos == nil {
+		return cpos
+	} else {
+		return *subPos
+	}
+}
diff --git a/pkg/shparse/shparse.go b/pkg/shparse/shparse.go
index 744260f06..ac09762f1 100644
--- a/pkg/shparse/shparse.go
+++ b/pkg/shparse/shparse.go
@@ -65,15 +65,6 @@ 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
@@ -525,243 +516,6 @@ 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
-
-	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
-	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 {
-	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)
-			rtn.CmdWordPos = idx - len(cmd.AssignmentWords)
-			rtn.CompType = CompTypeAssignment
-			return rtn
-		}
-		if pos <= endOffset {
-			// completing an assignment word
-			rtn.CmdWordPos = idx - len(cmd.AssignmentWords)
-			rtn.CompWord = word
-			rtn.CompWordOffset = pos - word.Offset
-			rtn.CompType = CompTypeAssignment
-			return rtn
-		}
-	}
-	var foundWord *WordType
-	var foundWordIdx int
-	for idx, word := range cmd.Words {
-		startOffset := word.Offset
-		endOffset := word.Offset + len(word.Raw)
-		if pos <= startOffset {
-			// starting a new word at this position
-			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
-			continue
-		}
-		if pos <= endOffset {
-			foundWord = word
-			foundWordIdx = idx
-			break
-		}
-	}
-	if foundWord != nil {
-		rtn.CmdWordPos = foundWordIdx
-		rtn.CompWord = foundWord
-		rtn.CompWordOffset = pos - foundWord.Offset
-		if foundWord.uncompletable() {
-			// invalid completion point
-			rtn.CompType = CompTypeInvalid
-			return rtn
-		}
-		rtn.CompType = compTypeFromPos(foundWordIdx)
-		return rtn
-	}
-	// past the end, so we're starting a new word in Cmd
-	rtn.CmdWordPos = len(cmd.Words)
-	rtn.CompType = CompTypeArg
-	return rtn
-}
-
-func (cmd *CmdType) findCompletionWordAtPos_none(pos int, superOffset int) CompletionPos {
-	rtn := CompletionPos{RawPos: pos, SuperOffset: superOffset}
-	if cmd.Type != CmdTypeNone {
-		panic("findWordAtPos_none only works for CmdTypeNone")
-	}
-	var foundWord *WordType
-	for _, word := range cmd.Words {
-		startOffset := word.Offset
-		endOffset := word.Offset + len(word.Raw)
-		if pos <= startOffset {
-			break
-		}
-		if pos <= endOffset {
-			if pos == endOffset && word.Type == WordTypeOp {
-				// operators are special, they can allow a full-word completion at endpos
-				continue
-			}
-			foundWord = word
-			break
-		}
-	}
-	if foundWord == nil {
-		// just revert to a file completion
-		rtn.CompType = CompTypeBasic
-		return rtn
-	}
-	rtn.CompWord = foundWord
-	rtn.CompWordOffset = pos - foundWord.Offset
-	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
-	}
-	// revert to file completion
-	rtn.CompType = CompTypeBasic
-	return rtn
-}
-
-func findWordAtPos(words []*WordType, pos int) *WordType {
-	for _, word := range words {
-		if pos > word.Offset && pos < word.Offset+len(word.Raw) {
-			return word
-		}
-	}
-	return nil
-}
-
-// recursively descend down the word, parse commands and find a sub completion point if any.
-// return nil if there is no sub completion point in this word
-func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos {
-	if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ {
-		// need to descend further
-		wmeta := wordMetaMap[word.Type]
-		if pos <= wmeta.PrefixLen {
-			return nil
-		}
-		endPos := len(word.Raw)
-		if word.Complete {
-			endPos = endPos - wmeta.SuffixLen
-		}
-		if pos >= endPos {
-			return nil
-		}
-		subWord := findWordAtPos(word.Subs, pos-wmeta.PrefixLen)
-		if subWord == nil {
-			return nil
-		}
-		fullOffset := subWord.Offset + wmeta.PrefixLen
-		return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset)
-	}
-	if word.Type == WordTypeDP || word.Type == WordTypeBQ {
-		wmeta := wordMetaMap[word.Type]
-		if pos < wmeta.PrefixLen {
-			return nil
-		}
-		if word.Complete && pos > len(word.Raw)-wmeta.SuffixLen {
-			return nil
-		}
-		subCmds := ParseCommands(word.Subs)
-		newPos := FindCompletionPos(subCmds, pos-wmeta.PrefixLen, superOffset+wmeta.PrefixLen)
-		return &newPos
-	}
-	return nil
-}
-
-// returns the context for completion
-// if we are completing in a simple-command, the returns the Cmd.  the Cmd can be used for specialized completion (command name, arg position, etc.)
-// 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
-		rtn.CompType = CompTypeCommand
-		return rtn
-	}
-	for _, cmd := range cmds {
-		endOffset := cmd.endOffset()
-		if pos > endOffset || (cmd.Type == CmdTypeNone && pos == endOffset) {
-			continue
-		}
-		startOffset := cmd.offset()
-		if cmd.Type == CmdTypeSimple {
-			if pos <= startOffset {
-				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 {
-				rtn.CompType = CompTypeCommand
-				return rtn
-			}
-			return cmd.findCompletionWordAtPos_none(pos, superOffset)
-		}
-	}
-	// past the end
-	lastCmd := cmds[len(cmds)-1]
-	if lastCmd.Type == CmdTypeSimple {
-		// just extend last command
-		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
-	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.CompWord == nil {
-		return cpos
-	}
-	subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset)
-	if subPos == nil {
-		return cpos
-	} else {
-		return *subPos
-	}
-}
-
 func ResetWordOffsets(words []*WordType, startIdx int) {
 	pos := startIdx
 	for _, word := range words {
diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go
index e4d7cc622..371791cdf 100644
--- a/pkg/shparse/shparse_test.go
+++ b/pkg/shparse/shparse_test.go
@@ -148,8 +148,9 @@ func TestCompPos(t *testing.T) {
 	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 -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)
 }
 
 func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) {