mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-01 18:28:59 +01:00
468 lines
12 KiB
Go
468 lines
12 KiB
Go
// scripthaus completion
|
|
package comp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/scripthaus-dev/mshell/pkg/packet"
|
|
"github.com/scripthaus-dev/mshell/pkg/shexec"
|
|
"github.com/scripthaus-dev/sh2-server/pkg/sstore"
|
|
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
|
|
"mvdan.cc/sh/v3/syntax"
|
|
)
|
|
|
|
const MaxCompQuoteLen = 5000
|
|
|
|
const (
|
|
// local to simplecomp
|
|
CGTypeCommand = "command"
|
|
CGTypeFile = "file"
|
|
CGTypeDir = "directory"
|
|
CGTypeVariable = "variable"
|
|
|
|
// implemented in cmdrunner
|
|
CGTypeMeta = "metacmd"
|
|
CGTypeCommandMeta = "command+meta"
|
|
|
|
CGTypeRemote = "remote"
|
|
CGTypeRemoteInstance = "remoteinstance"
|
|
CGTypeGlobalCmd = "globalcmd"
|
|
)
|
|
|
|
const (
|
|
QuoteTypeLiteral = ""
|
|
QuoteTypeDQ = "\""
|
|
QuoteTypeANSI = "$'"
|
|
QuoteTypeSQ = "'"
|
|
)
|
|
|
|
type CompContext struct {
|
|
RemotePtr *sstore.RemotePtrType
|
|
State *packet.ShellState
|
|
ForDisplay bool
|
|
}
|
|
|
|
type StrWithPos struct {
|
|
Str string
|
|
Pos int
|
|
}
|
|
|
|
type ParsedWord struct {
|
|
Offset int
|
|
Word *syntax.Word
|
|
PartialWord string
|
|
Prefix string
|
|
}
|
|
|
|
type CompPoint struct {
|
|
StmtStr string
|
|
Words []ParsedWord
|
|
CompWord int
|
|
CompWordPos int
|
|
Prefix string
|
|
Suffix string
|
|
}
|
|
|
|
// directories will have a trailing "/"
|
|
type CompEntry struct {
|
|
Word string
|
|
IsMetaCmd bool
|
|
}
|
|
|
|
type CompReturn struct {
|
|
CompType string
|
|
Entries []CompEntry
|
|
HasMore bool
|
|
}
|
|
|
|
func compQuoteDQString(s string, close bool) string {
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('"')
|
|
for _, ch := range s {
|
|
if ch == '"' || ch == '\\' || ch == '$' || ch == '`' {
|
|
buf.WriteByte('\\')
|
|
buf.WriteRune(ch)
|
|
continue
|
|
}
|
|
buf.WriteRune(ch)
|
|
}
|
|
if close {
|
|
buf.WriteByte('"')
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func compQuoteString(s string, quoteType string, close bool) string {
|
|
if quoteType != QuoteTypeANSI {
|
|
for _, ch := range s {
|
|
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) || ch == '!' {
|
|
quoteType = QuoteTypeANSI
|
|
break
|
|
}
|
|
if ch == '\'' {
|
|
if quoteType == QuoteTypeSQ || quoteType == QuoteTypeLiteral {
|
|
quoteType = QuoteTypeANSI
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if quoteType == QuoteTypeANSI {
|
|
rtn := strconv.QuoteToASCII(s)
|
|
rtn = "$'" + strings.ReplaceAll(rtn[1:len(rtn)-1], "'", "\\'")
|
|
if close {
|
|
rtn = rtn + "'"
|
|
}
|
|
return rtn
|
|
}
|
|
if quoteType == QuoteTypeLiteral {
|
|
rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen)
|
|
if len(rtn) > 0 && rtn[0] == '\'' && !close {
|
|
rtn = rtn[0 : len(rtn)-1]
|
|
}
|
|
return rtn
|
|
}
|
|
if quoteType == QuoteTypeSQ {
|
|
rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen)
|
|
if len(rtn) > 0 && rtn[0] != '\'' {
|
|
rtn = "'" + rtn + "'"
|
|
}
|
|
if !close {
|
|
rtn = rtn[0 : len(rtn)-1]
|
|
}
|
|
return rtn
|
|
}
|
|
// QuoteTypeDQ
|
|
return compQuoteDQString(s, close)
|
|
}
|
|
|
|
func (p *CompPoint) wordAsStr(w ParsedWord) string {
|
|
if w.Word != nil {
|
|
return p.StmtStr[w.Word.Pos().Offset():w.Word.End().Offset()]
|
|
}
|
|
return w.PartialWord
|
|
}
|
|
|
|
func (p *CompPoint) simpleExpandWord(w ParsedWord) string {
|
|
ectx := shexec.SimpleExpandContext{}
|
|
if w.Word != nil {
|
|
return shexec.SimpleExpandWord(ectx, w.Word, p.StmtStr)
|
|
}
|
|
return shexec.SimpleExpandPartialWord(ectx, w.PartialWord, false)
|
|
}
|
|
|
|
func getQuoteTypePref(str string) string {
|
|
if strings.HasPrefix(str, QuoteTypeANSI) {
|
|
return QuoteTypeANSI
|
|
}
|
|
if strings.HasPrefix(str, QuoteTypeDQ) {
|
|
return QuoteTypeDQ
|
|
}
|
|
if strings.HasPrefix(str, QuoteTypeSQ) {
|
|
return QuoteTypeSQ
|
|
}
|
|
return QuoteTypeLiteral
|
|
}
|
|
|
|
func (p *CompPoint) getCompPrefix() string {
|
|
if p.CompWordPos == 0 {
|
|
return ""
|
|
}
|
|
pword := p.Words[p.CompWord]
|
|
wordStr := p.wordAsStr(pword)
|
|
if p.CompWordPos == len(wordStr) {
|
|
return p.simpleExpandWord(pword)
|
|
}
|
|
// TODO we can do better, if p.Word is not nil, we can look for which WordPart
|
|
// our pos is in. we can then do a normal word expand on the previous parts
|
|
// and a partial on just the current part. this is an uncommon case though
|
|
// and has very little upside (even bash does not expand multipart words correctly)
|
|
partialWordStr := wordStr[:p.CompWordPos]
|
|
return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false)
|
|
}
|
|
|
|
func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos {
|
|
pword := p.Words[p.CompWord]
|
|
wordStr := p.wordAsStr(pword)
|
|
quotePref := getQuoteTypePref(wordStr)
|
|
needsClose := newWordComplete && (len(wordStr) == p.CompWordPos)
|
|
wordSuffix := wordStr[p.CompWordPos:]
|
|
newQuotedStr := compQuoteString(newWord, quotePref, needsClose)
|
|
if needsClose && wordSuffix == "" && !strings.HasSuffix(newWord, "/") {
|
|
newQuotedStr = newQuotedStr + " "
|
|
}
|
|
newPos := len(newQuotedStr)
|
|
return StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos}
|
|
}
|
|
|
|
func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos {
|
|
if crtn == nil || crtn.HasMore {
|
|
return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
|
|
}
|
|
compStrs := crtn.GetCompStrs()
|
|
compPrefix := p.getCompPrefix()
|
|
lcp := utilfn.LongestPrefix(compPrefix, compStrs)
|
|
if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) {
|
|
return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
|
|
}
|
|
newStr := p.extendWord(lcp, utilfn.ContainsStr(compStrs, lcp))
|
|
var buf bytes.Buffer
|
|
buf.WriteString(p.Prefix)
|
|
for idx, w := range p.Words {
|
|
if idx == p.CompWord {
|
|
buf.WriteString(w.Prefix)
|
|
buf.WriteString(newStr.Str)
|
|
} else {
|
|
buf.WriteString(w.Prefix)
|
|
buf.WriteString(p.wordAsStr(w))
|
|
}
|
|
}
|
|
buf.WriteString(p.Suffix)
|
|
compWord := p.Words[p.CompWord]
|
|
newPos := len(p.Prefix) + compWord.Offset + len(compWord.Prefix) + newStr.Pos
|
|
return StrWithPos{Str: buf.String(), Pos: newPos}
|
|
}
|
|
|
|
func (p *CompPoint) dump() {
|
|
if p.Prefix != "" {
|
|
fmt.Printf("prefix: %s\n", p.Prefix)
|
|
}
|
|
fmt.Printf("cpos: %d %d\n", p.CompWord, p.CompWordPos)
|
|
for idx, w := range p.Words {
|
|
fmt.Printf("w[%d]: ", idx)
|
|
if w.Prefix != "" {
|
|
fmt.Printf("{%s}", w.Prefix)
|
|
}
|
|
if idx == p.CompWord {
|
|
fmt.Printf("%s\n", strWithCursor(p.wordAsStr(w), p.CompWordPos))
|
|
} else {
|
|
fmt.Printf("%s\n", p.wordAsStr(w))
|
|
}
|
|
}
|
|
if p.Suffix != "" {
|
|
fmt.Printf("suffix: %s\n", p.Suffix)
|
|
}
|
|
fmt.Printf("\n")
|
|
}
|
|
|
|
var SimpleCompGenFns map[string]SimpleCompGenFnType
|
|
|
|
func (sp StrWithPos) String() string {
|
|
return strWithCursor(sp.Str, sp.Pos)
|
|
}
|
|
|
|
func strWithCursor(str string, pos int) string {
|
|
if pos < 0 {
|
|
return "[*]_" + str
|
|
}
|
|
if pos >= len(str) {
|
|
if pos > len(str) {
|
|
return str + "_[*]"
|
|
}
|
|
return str + "[*]"
|
|
} else {
|
|
return str[:pos] + "[*]" + str[pos:]
|
|
}
|
|
}
|
|
|
|
func isWhitespace(str string) bool {
|
|
return strings.TrimSpace(str) == ""
|
|
}
|
|
|
|
func splitInitialWhitespace(str string) (string, string) {
|
|
for pos, ch := range str { // rune iteration :/
|
|
if !unicode.IsSpace(ch) {
|
|
return str[:pos], str[pos:]
|
|
}
|
|
}
|
|
return str, ""
|
|
}
|
|
|
|
func ParseCompPoint(cmdStr StrWithPos) *CompPoint {
|
|
fullCmdStr := cmdStr.Str
|
|
pos := cmdStr.Pos
|
|
// fmt.Printf("---\n")
|
|
// fmt.Printf("cmd: %s\n", strWithCursor(fullCmdStr, pos))
|
|
|
|
// first, find the stmt that the pos appears in
|
|
cmdReader := strings.NewReader(fullCmdStr)
|
|
parser := syntax.NewParser(syntax.Variant(syntax.LangBash))
|
|
var foundStmt *syntax.Stmt
|
|
var lastStmt *syntax.Stmt
|
|
var restStartPos int
|
|
parser.Stmts(cmdReader, func(stmt *syntax.Stmt) bool { // ignore parse errors (since stmtStr will be the unparsed part)
|
|
restStartPos = int(stmt.End().Offset())
|
|
lastStmt = stmt
|
|
if uint(pos) >= stmt.Pos().Offset() && uint(pos) < stmt.End().Offset() {
|
|
foundStmt = stmt
|
|
return false
|
|
}
|
|
// fmt.Printf("stmt: [[%s]] %d:%d (%d)\n", fullCmdStr[stmt.Pos().Offset():stmt.End().Offset()], stmt.Pos().Offset(), stmt.End().Offset(), stmt.Semicolon.Offset())
|
|
return true
|
|
})
|
|
restStr := fullCmdStr[restStartPos:]
|
|
if foundStmt == nil && lastStmt != nil && isWhitespace(restStr) && lastStmt.Semicolon.Offset() == 0 {
|
|
foundStmt = lastStmt
|
|
}
|
|
var rtnPoint CompPoint
|
|
var stmtStr string
|
|
var stmtPos int
|
|
if foundStmt != nil {
|
|
stmtPos = pos - int(foundStmt.Pos().Offset())
|
|
rtnPoint.Prefix = fullCmdStr[:foundStmt.Pos().Offset()]
|
|
if isWhitespace(fullCmdStr[foundStmt.End().Offset():]) {
|
|
stmtStr = fullCmdStr[foundStmt.Pos().Offset():]
|
|
rtnPoint.Suffix = ""
|
|
} else {
|
|
stmtStr = fullCmdStr[foundStmt.Pos().Offset():foundStmt.End().Offset()]
|
|
rtnPoint.Suffix = fullCmdStr[foundStmt.End().Offset():]
|
|
}
|
|
} else {
|
|
stmtStr = restStr
|
|
stmtPos = pos - restStartPos
|
|
rtnPoint.Prefix = fullCmdStr[:restStartPos]
|
|
rtnPoint.Suffix = fullCmdStr[restStartPos+len(stmtStr):]
|
|
}
|
|
if stmtPos > len(stmtStr) {
|
|
// this should not happen and will cause a jump in completed strings
|
|
stmtPos = len(stmtStr)
|
|
}
|
|
// fmt.Printf("found: ((%s))%s((%s))\n", rtnPoint.Prefix, strWithCursor(stmtStr, stmtPos), rtnPoint.Suffix)
|
|
|
|
// now, find the word that the pos appears in within the stmt above
|
|
rtnPoint.StmtStr = stmtStr
|
|
stmtReader := strings.NewReader(stmtStr)
|
|
lastWordPos := 0
|
|
parser.Words(stmtReader, func(w *syntax.Word) bool {
|
|
var pword ParsedWord
|
|
pword.Offset = lastWordPos
|
|
if int(w.Pos().Offset()) > lastWordPos {
|
|
pword.Prefix = stmtStr[lastWordPos:w.Pos().Offset()]
|
|
}
|
|
pword.Word = w
|
|
rtnPoint.Words = append(rtnPoint.Words, pword)
|
|
lastWordPos = int(w.End().Offset())
|
|
return true
|
|
})
|
|
if lastWordPos < len(stmtStr) {
|
|
pword := ParsedWord{Offset: lastWordPos}
|
|
pword.Prefix, pword.PartialWord = splitInitialWhitespace(stmtStr[lastWordPos:])
|
|
rtnPoint.Words = append(rtnPoint.Words, pword)
|
|
}
|
|
if len(rtnPoint.Words) == 0 {
|
|
rtnPoint.Words = append(rtnPoint.Words, ParsedWord{})
|
|
}
|
|
for idx, w := range rtnPoint.Words {
|
|
wordLen := len(rtnPoint.wordAsStr(w))
|
|
if stmtPos > w.Offset && stmtPos <= w.Offset+len(w.Prefix)+wordLen {
|
|
rtnPoint.CompWord = idx
|
|
rtnPoint.CompWordPos = stmtPos - w.Offset - len(w.Prefix)
|
|
if rtnPoint.CompWordPos < 0 {
|
|
splitCompWord(&rtnPoint)
|
|
}
|
|
}
|
|
}
|
|
return &rtnPoint
|
|
}
|
|
|
|
func splitCompWord(p *CompPoint) {
|
|
w := p.Words[p.CompWord]
|
|
prefixPos := p.CompWordPos + len(w.Prefix)
|
|
|
|
w1 := ParsedWord{Offset: w.Offset, Prefix: w.Prefix[:prefixPos]}
|
|
w2 := ParsedWord{Offset: w.Offset + prefixPos, Prefix: w.Prefix[prefixPos:], Word: w.Word, PartialWord: w.PartialWord}
|
|
p.CompWord = p.CompWord // the same (w1)
|
|
p.CompWordPos = 0 // will be at 0 since w1 has a word length of 0
|
|
var newWords []ParsedWord
|
|
if p.CompWord > 0 {
|
|
newWords = append(newWords, p.Words[0:p.CompWord]...)
|
|
}
|
|
newWords = append(newWords, w1, w2)
|
|
newWords = append(newWords, p.Words[p.CompWord+1:]...)
|
|
p.Words = newWords
|
|
}
|
|
|
|
func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) {
|
|
compPoint := ParseCompPoint(sp)
|
|
compType := CGTypeFile
|
|
if compPoint.CompWord == 0 {
|
|
compType = CGTypeCommandMeta
|
|
}
|
|
// TODO lookup special types
|
|
compPrefix := compPoint.getCompPrefix()
|
|
crtn, err := DoSimpleComp(ctx, compType, compPrefix, compCtx, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if compCtx.ForDisplay {
|
|
return crtn, nil, nil
|
|
}
|
|
rtnSP := compPoint.FullyExtend(crtn)
|
|
return crtn, &rtnSP, nil
|
|
}
|
|
|
|
func SortCompReturnEntries(c *CompReturn) {
|
|
sort.Slice(c.Entries, func(i int, j int) bool {
|
|
e1 := c.Entries[i]
|
|
e2 := c.Entries[j]
|
|
if e1.Word < e2.Word {
|
|
return true
|
|
}
|
|
if e1.Word == e2.Word && e1.IsMetaCmd && !e2.IsMetaCmd {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
func CombineCompReturn(compType string, c1 *CompReturn, c2 *CompReturn) *CompReturn {
|
|
if c1 == nil {
|
|
return c2
|
|
}
|
|
if c2 == nil {
|
|
return c1
|
|
}
|
|
var rtn CompReturn
|
|
rtn.CompType = compType
|
|
rtn.HasMore = c1.HasMore || c2.HasMore
|
|
rtn.Entries = append([]CompEntry{}, c1.Entries...)
|
|
rtn.Entries = append(rtn.Entries, c2.Entries...)
|
|
SortCompReturnEntries(&rtn)
|
|
return &rtn
|
|
}
|
|
|
|
func (c *CompReturn) GetCompStrs() []string {
|
|
rtn := make([]string, len(c.Entries))
|
|
for idx, entry := range c.Entries {
|
|
rtn[idx] = entry.Word
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func (c *CompReturn) GetCompDisplayStrs() []string {
|
|
rtn := make([]string, len(c.Entries))
|
|
for idx, entry := range c.Entries {
|
|
if entry.IsMetaCmd {
|
|
rtn[idx] = "^" + entry.Word
|
|
} else {
|
|
rtn[idx] = entry.Word
|
|
}
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func (p CompPoint) getOrigPos() int {
|
|
pword := p.Words[p.CompWord]
|
|
return len(p.Prefix) + pword.Offset + len(pword.Prefix) + p.CompWordPos
|
|
}
|
|
|
|
func (p CompPoint) getOrigStr() string {
|
|
return p.Prefix + p.StmtStr + p.Suffix
|
|
}
|