mirror of https://github.com/chzyer/readline.git
feat: Pagination for tab completion
This commit is contained in:
parent
7f93d88cd5
commit
104a7c3038
159
complete.go
159
complete.go
|
@ -36,13 +36,18 @@ type opCompleter struct {
|
||||||
candidateOff int
|
candidateOff int
|
||||||
candidateChoise int
|
candidateChoise int
|
||||||
candidateColNum int
|
candidateColNum int
|
||||||
|
|
||||||
|
// State for pagination
|
||||||
|
maxLine int // Maximum allowed columns on a single terminal
|
||||||
|
pageIdx int // Current page
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
||||||
return &opCompleter{
|
return &opCompleter{
|
||||||
w: w,
|
w: w,
|
||||||
op: op,
|
op: op,
|
||||||
width: width,
|
width: width,
|
||||||
|
maxLine: 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +62,48 @@ func (o *opCompleter) doSelect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *opCompleter) nextCandidate(i int) {
|
func (o *opCompleter) nextCandidate(i int) {
|
||||||
o.candidateChoise += i
|
// Number of elements in a full screen
|
||||||
o.candidateChoise = o.candidateChoise % len(o.candidate)
|
numCandidatePerPage := o.candidateColNum * o.maxLine
|
||||||
if o.candidateChoise < 0 {
|
// Number of elements in the current screen, which could be non-full
|
||||||
o.candidateChoise = len(o.candidate) + o.candidateChoise
|
matrixSize := o.numCandidatesCurPage()
|
||||||
|
pageStart := o.pageIdx * numCandidatePerPage
|
||||||
|
|
||||||
|
candidateChoiceModPage := o.candidateChoise - pageStart
|
||||||
|
candidateChoiceModPage += i
|
||||||
|
candidateChoiceModPage %= matrixSize
|
||||||
|
if candidateChoiceModPage < 0 {
|
||||||
|
candidateChoiceModPage += matrixSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.candidateChoise = candidateChoiceModPage + pageStart
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) nextLine(i int) {
|
||||||
|
// Number of elements in a full screen
|
||||||
|
candidatesCurPage := o.numCandidatesCurPage()
|
||||||
|
numCandidatesFullPage := o.maxLine * o.candidateColNum
|
||||||
|
pageStart := o.pageIdx * numCandidatesFullPage
|
||||||
|
|
||||||
|
// Number of elements in the current screen, which could be non-full
|
||||||
|
numLines := o.getLinesCurPage()
|
||||||
|
rectangleSize := numLines * o.candidateColNum
|
||||||
|
|
||||||
|
candidateChoiceModPage := o.candidateChoise - pageStart
|
||||||
|
candidateChoiceModPage += i * o.candidateColNum
|
||||||
|
|
||||||
|
if candidateChoiceModPage >= candidatesCurPage {
|
||||||
|
if candidateChoiceModPage < rectangleSize {
|
||||||
|
candidateChoiceModPage += o.candidateColNum
|
||||||
|
}
|
||||||
|
candidateChoiceModPage -= rectangleSize
|
||||||
|
} else if candidateChoiceModPage < 0 {
|
||||||
|
candidateChoiceModPage += rectangleSize
|
||||||
|
if candidateChoiceModPage > candidatesCurPage {
|
||||||
|
candidateChoiceModPage -= o.candidateColNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o.candidateChoise = candidateChoiceModPage + pageStart
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *opCompleter) OnComplete() bool {
|
func (o *opCompleter) OnComplete() bool {
|
||||||
|
@ -143,25 +185,15 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||||||
o.ExitCompleteMode(true)
|
o.ExitCompleteMode(true)
|
||||||
next = false
|
next = false
|
||||||
case CharNext:
|
case CharNext:
|
||||||
tmpChoise := o.candidateChoise + o.candidateColNum
|
o.nextLine(1)
|
||||||
if tmpChoise >= o.getMatrixSize() {
|
|
||||||
tmpChoise -= o.getMatrixSize()
|
|
||||||
} else if tmpChoise >= len(o.candidate) {
|
|
||||||
tmpChoise += o.candidateColNum
|
|
||||||
tmpChoise -= o.getMatrixSize()
|
|
||||||
}
|
|
||||||
o.candidateChoise = tmpChoise
|
|
||||||
case CharBackward:
|
case CharBackward:
|
||||||
o.nextCandidate(-1)
|
o.nextCandidate(-1)
|
||||||
case CharPrev:
|
case CharPrev:
|
||||||
tmpChoise := o.candidateChoise - o.candidateColNum
|
o.nextLine(-1)
|
||||||
if tmpChoise < 0 {
|
case CharK:
|
||||||
tmpChoise += o.getMatrixSize()
|
o.updatePage(1)
|
||||||
if tmpChoise >= len(o.candidate) {
|
case CharJ:
|
||||||
tmpChoise -= o.candidateColNum
|
o.updatePage(-1)
|
||||||
}
|
|
||||||
}
|
|
||||||
o.candidateChoise = tmpChoise
|
|
||||||
default:
|
default:
|
||||||
next = false
|
next = false
|
||||||
o.ExitCompleteSelectMode()
|
o.ExitCompleteSelectMode()
|
||||||
|
@ -173,18 +205,50 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *opCompleter) getMatrixSize() int {
|
// Number of candidate completions we can show on the current page,
|
||||||
line := len(o.candidate) / o.candidateColNum
|
// which might be different from number of candidates on a full page if
|
||||||
if len(o.candidate)%o.candidateColNum != 0 {
|
// we are on the last page.
|
||||||
line++
|
func (o *opCompleter) numCandidatesCurPage() int {
|
||||||
|
numCandidatePerPage := o.candidateColNum * o.maxLine
|
||||||
|
pageStart := o.pageIdx * numCandidatePerPage
|
||||||
|
if len(o.candidate)-pageStart >= numCandidatePerPage {
|
||||||
|
return numCandidatePerPage
|
||||||
}
|
}
|
||||||
return line * o.candidateColNum
|
return len(o.candidate) - pageStart
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of lines on the current page
|
||||||
|
func (o *opCompleter) getLinesCurPage() int {
|
||||||
|
curPageSize := o.numCandidatesCurPage()
|
||||||
|
numLines := curPageSize / o.candidateColNum
|
||||||
|
if curPageSize%o.candidateColNum != 0 {
|
||||||
|
numLines += 1
|
||||||
|
}
|
||||||
|
return numLines
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *opCompleter) OnWidthChange(newWidth int) {
|
func (o *opCompleter) OnWidthChange(newWidth int) {
|
||||||
o.width = newWidth
|
o.width = newWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move page
|
||||||
|
func (o *opCompleter) updatePage(offset int) {
|
||||||
|
if !o.inCompleteMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageIdx := o.pageIdx + offset
|
||||||
|
if nextPageIdx < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPageStart := nextPageIdx * o.candidateColNum * o.maxLine
|
||||||
|
if nextPageStart > len(o.candidate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.pageIdx = nextPageIdx
|
||||||
|
o.candidateChoise = nextPageStart
|
||||||
|
}
|
||||||
|
|
||||||
func (o *opCompleter) CompleteRefresh() {
|
func (o *opCompleter) CompleteRefresh() {
|
||||||
if !o.inCompleteMode {
|
if !o.inCompleteMode {
|
||||||
return
|
return
|
||||||
|
@ -214,7 +278,17 @@ func (o *opCompleter) CompleteRefresh() {
|
||||||
colIdx := 0
|
colIdx := 0
|
||||||
lines := 1
|
lines := 1
|
||||||
buf.WriteString("\033[J")
|
buf.WriteString("\033[J")
|
||||||
for idx, c := range o.candidate {
|
|
||||||
|
// Compute the candidates to show on the current page
|
||||||
|
numCandidatePerPage := o.candidateColNum * o.maxLine
|
||||||
|
startIdx := o.pageIdx * numCandidatePerPage
|
||||||
|
endIdx := (o.pageIdx + 1) * numCandidatePerPage
|
||||||
|
if endIdx > len(o.candidate) {
|
||||||
|
endIdx = len(o.candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := startIdx; idx < endIdx; idx += 1 {
|
||||||
|
c := o.candidate[idx]
|
||||||
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
|
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
|
||||||
if inSelect {
|
if inSelect {
|
||||||
buf.WriteString("\033[30;47m")
|
buf.WriteString("\033[30;47m")
|
||||||
|
@ -235,6 +309,15 @@ func (o *opCompleter) CompleteRefresh() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add an extra line for navigation instructions
|
||||||
|
if colIdx != 0 {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
navigationMsg := "(j: prev page) (k: next page)"
|
||||||
|
buf.WriteString(navigationMsg)
|
||||||
|
buf.Write(bytes.Repeat([]byte(" "), width-len(navigationMsg)))
|
||||||
|
|
||||||
// move back
|
// move back
|
||||||
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
|
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
|
||||||
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
|
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
|
||||||
|
@ -268,6 +351,24 @@ func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
|
||||||
o.inCompleteMode = true
|
o.inCompleteMode = true
|
||||||
o.candidate = candidate
|
o.candidate = candidate
|
||||||
o.candidateOff = offset
|
o.candidateOff = offset
|
||||||
|
|
||||||
|
// Initialize for complete mode
|
||||||
|
colWidth := 0
|
||||||
|
for _, c := range candidate {
|
||||||
|
w := runes.WidthAll(c)
|
||||||
|
if w > colWidth {
|
||||||
|
colWidth = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colWidth += offset + 1
|
||||||
|
width := o.width - 1
|
||||||
|
colNum := width / colWidth
|
||||||
|
if colNum != 0 {
|
||||||
|
colWidth += (width - (colWidth * colNum)) / colNum
|
||||||
|
}
|
||||||
|
o.candidateColNum = colNum
|
||||||
|
o.pageIdx = 0
|
||||||
|
|
||||||
o.CompleteRefresh()
|
o.CompleteRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
|
func randSeq(n int) string {
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A completor that will give a lot of completions for showcasing the paging functionality
|
||||||
|
type Completor struct{}
|
||||||
|
|
||||||
|
func (c *Completor) Do(line []rune, pos int) ([][]rune, int) {
|
||||||
|
completion := make([][]rune, 0, 10000)
|
||||||
|
for i := range 10000 {
|
||||||
|
s := fmt.Sprintf("%s%020d", randSeq(1), i)
|
||||||
|
completion = append(completion, []rune(s))
|
||||||
|
}
|
||||||
|
return completion, pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := Completor{}
|
||||||
|
l, err := readline.NewEx(&readline.Config{
|
||||||
|
Prompt: "\033[31m»\033[0m ",
|
||||||
|
AutoComplete: &c,
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
EOFPrompt: "exit",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
for {
|
||||||
|
line, err := l.Readline()
|
||||||
|
if err == readline.ErrInterrupt {
|
||||||
|
if len(line) == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
log.Println("you said:", strconv.Quote(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module github.com/chzyer/readline
|
module github.com/chzyer/readline
|
||||||
|
|
||||||
go 1.15
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/chzyer/test v1.0.0
|
github.com/chzyer/test v1.0.0
|
||||||
|
|
Loading…
Reference in New Issue