diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go index ffac6229..1c4be625 100644 --- a/cmd/wasm/main.go +++ b/cmd/wasm/main.go @@ -15,6 +15,21 @@ var ( r *repl.REPL ) +func zcCommonPrefix() js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) != 1 { + panic("zcCommonPrefix: invalid number of arguments") + } + jsValues := args[0] + var outValues []string + for i := 0; i < jsValues.Length(); i++ { + outValues = append(outValues, jsValues.Index(i).String()) + } + common := repl.CommonPrefix(outValues) + return common + }) +} + func zcEval() js.Func { return js.FuncOf(func(this js.Value, args []js.Value) any { in := args[0].String() @@ -77,14 +92,36 @@ func zcSetStack() js.Func { }) } +func zcWordCompleter() js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) != 2 { + panic("zcWordCompleter: invalid number of arguments") + } + line := args[0].String() + pos := args[1].Int() + prefix, candidates, suffix := r.WordCompleter(line, pos) + var jsCandidates []any + for _, c := range candidates { + jsCandidates = append(jsCandidates, c) + } + return map[string]any{ + "prefix": prefix, + "candidates": jsCandidates, + "suffix": suffix, + } + }) +} + func main() { c = calc.New() r = repl.New(c) + js.Global().Set("zcCommonPrefix", zcCommonPrefix()) js.Global().Set("zcEval", zcEval()) js.Global().Set("zcStack", zcStack()) js.Global().Set("zcStackLen", zcStackLen()) js.Global().Set("zcOpNames", zcOpNames()) js.Global().Set("zcSetStack", zcSetStack()) + js.Global().Set("zcWordCompleter", zcWordCompleter()) <-make(chan struct{}) } diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index dba2b33b..85e6562c 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -63,7 +63,7 @@ func (r *REPL) Init() { r.cli = liner.NewLiner() r.cli.SetCtrlCAborts(true) r.cli.SetTabCompletionStyle(liner.TabPrints) - r.cli.SetWordCompleter(r.wordCompleter) + r.cli.SetWordCompleter(r.WordCompleter) r.loadHistory() @@ -181,13 +181,12 @@ func (r *REPL) getPrompt() string { return zc.ProgName + " > " } -func (r *REPL) wordCompleter(line string, pos int) (string, []string, string) { +func (r *REPL) WordCompleter(line string, pos int) (string, []string, string) { endPos := pos for endPos < len(line) { if line[endPos] == ' ' { break } - endPos++ } startPos := pos - 1 if startPos < 0 { @@ -214,10 +213,38 @@ func (r *REPL) wordCompleter(line string, pos int) (string, []string, string) { } } sort.Strings(candidates) - //fmt.Printf("\n[%v] (%v)[%v] [%v]\n", prefix, word, candidates, suffix) return prefix, candidates, suffix } +func CommonPrefix(vals []string) string { + if len(vals) == 0 { + return "" + } + var result []rune + for i, sval := range vals { + val := []rune(sval) + if i == 0 { + result = val + continue + } + if len(result) == 0 { + return "" + } + for j, a := range result { + if j >= len(val) { + result = result[:j] + break + } + b := val[j] + if a != b { + result = result[:j] + break + } + } + } + return string(result) +} + func colorize(color string, text string) string { if !ansi.Enabled { return zc.FormatStackItem(text) diff --git a/pkg/repl/repl_test.go b/pkg/repl/repl_test.go index 74f75060..280c64cd 100644 --- a/pkg/repl/repl_test.go +++ b/pkg/repl/repl_test.go @@ -69,3 +69,26 @@ func TestQuote(t *testing.T) { t.Fatalf("\n have: %v \n want: %v", have, want) } } + +func TestCommonPrefix(t *testing.T) { + tests := []struct { + common string + vals []string + }{ + {"abc", []string{"abc", "abc", "abc"}}, + {"a", []string{"abc", "ab", "a"}}, + {"a", []string{"a", "ab", "abc"}}, + {"", []string{"a", "b", "c"}}, + {"abc", []string{"abcde", "abcfg", "abch"}}, + {"char-c", []string{"char-codepoint", "char-cp"}}, + } + + for _, test := range tests { + t.Run(test.common, func(t *testing.T) { + common := CommonPrefix(test.vals) + if common != test.common { + t.Errorf("\n have: %v \n want: %v", common, test.common) + } + }) + } +} diff --git a/web/zc.js b/web/zc.js index 0cd71a00..cb40b907 100644 --- a/web/zc.js +++ b/web/zc.js @@ -1,7 +1,9 @@ var stackHist = [] var commandHist = [] var histPos = -1 -var showCandidates = false + +// var showCandidates = false +var tabs = 0 function submit() { let line = document.querySelector("#input").value @@ -97,41 +99,32 @@ function moveToEnd() { function autoComplete() { let e = document.querySelector('#input') - let pos = e.selectionEnd - 1 - if (pos <= 0) { - pos = 0 - } - let searchFor = '' - for (let i = pos; i >= 0; i--) { - if (e.value[i] === ' ') { - break - } - searchFor = e.value[i] + searchFor - } - if (searchFor === '') { + + if (e.value.trim().length === 0) { return } - let candidates = zcOpNames().filter((e) => e.startsWith(searchFor)) - if (candidates.length > 50) { - candidates = candidates.slice(0, 50) - candidates.push("...") - } + console.log('value', e.value, 'pos', e.selectionEnd) + let r = zcWordCompleter(e.value, e.selectionEnd) + console.log(r) + let common = zcCommonPrefix(r.candidates) + console.log(common) - if (candidates.length === 0) { - showCandidates = false - } else if (candidates.length === 1) { - let toAdd = candidates[0].slice(searchFor.length) - let head = e.value.slice(0, pos + 1) - let tail = e.value.slice(pos + 1) - e.value = head + toAdd + tail - e.selectionStart = e.selectionEnd = pos + toAdd.length + 1 - } else if (!showCandidates) { - showCandidates = true + var middle = '' + if (r.candidates.length === 0) { + tabs = 0 + } else if (r.candidates.length == 1) { + middle = r.candidates[0] + tabs = 0 } else { - candidates = candidates.map((e) => e.replace("&", "&")) - document.querySelector("#popup").innerHTML = candidates.join(' ') + middle = common + if (tabs >= 2) { + let candidates = r.candidates.map((e) => e.replace("&", "&")) + document.querySelector("#popup").innerHTML = candidates.join(' ') + } } + e.value = r.prefix + middle + r.suffix + e.selectionStart = e.selectionEnd = r.prefix.length + middle.length } function clearPopup() { @@ -162,14 +155,19 @@ window.onload = function() { document.querySelector('#input').onkeydown = function(evt) { clearPopup() let keyCode = evt.code || evt.key + + if (keyCode === 'Tab') { + tabs++ + } else { + tabs = 0 + } + if (keyCode === 'ArrowUp') { up() clearPopup() - showCandidates = false } else if (keyCode === 'ArrowDown') { down() - clearPopup(); - showCandidates = false + clearPopup() } else if (keyCode === 'Tab') { autoComplete() evt.preventDefault() @@ -179,7 +177,7 @@ window.onload = function() { } document.querySelector('#auto').onclick = function(evt) { - showCandidates = true + tabs++ autoComplete() document.querySelector('#input').focus() }