feat: Pagination for tab completion

This commit is contained in:
Yangchen Ye 2024-08-03 15:33:57 -04:00
parent 7f93d88cd5
commit 104a7c3038
4 changed files with 199 additions and 30 deletions

View File

@ -36,6 +36,10 @@ type opCompleter struct {
candidateOff int
candidateChoise 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 {
@ -43,6 +47,7 @@ func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
w: w,
op: op,
width: width,
maxLine: 5,
}
}
@ -57,11 +62,48 @@ func (o *opCompleter) doSelect() {
}
func (o *opCompleter) nextCandidate(i int) {
o.candidateChoise += i
o.candidateChoise = o.candidateChoise % len(o.candidate)
if o.candidateChoise < 0 {
o.candidateChoise = len(o.candidate) + o.candidateChoise
// Number of elements in a full screen
numCandidatePerPage := o.candidateColNum * o.maxLine
// Number of elements in the current screen, which could be non-full
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 {
@ -143,25 +185,15 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
o.ExitCompleteMode(true)
next = false
case CharNext:
tmpChoise := o.candidateChoise + o.candidateColNum
if tmpChoise >= o.getMatrixSize() {
tmpChoise -= o.getMatrixSize()
} else if tmpChoise >= len(o.candidate) {
tmpChoise += o.candidateColNum
tmpChoise -= o.getMatrixSize()
}
o.candidateChoise = tmpChoise
o.nextLine(1)
case CharBackward:
o.nextCandidate(-1)
case CharPrev:
tmpChoise := o.candidateChoise - o.candidateColNum
if tmpChoise < 0 {
tmpChoise += o.getMatrixSize()
if tmpChoise >= len(o.candidate) {
tmpChoise -= o.candidateColNum
}
}
o.candidateChoise = tmpChoise
o.nextLine(-1)
case CharK:
o.updatePage(1)
case CharJ:
o.updatePage(-1)
default:
next = false
o.ExitCompleteSelectMode()
@ -173,18 +205,50 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
return false
}
func (o *opCompleter) getMatrixSize() int {
line := len(o.candidate) / o.candidateColNum
if len(o.candidate)%o.candidateColNum != 0 {
line++
// Number of candidate completions we can show on the current page,
// which might be different from number of candidates on a full page if
// we are on the last page.
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) {
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() {
if !o.inCompleteMode {
return
@ -214,7 +278,17 @@ func (o *opCompleter) CompleteRefresh() {
colIdx := 0
lines := 1
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()
if inSelect {
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
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
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.candidate = candidate
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()
}

View File

@ -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
View File

@ -1,6 +1,6 @@
module github.com/chzyer/readline
go 1.15
go 1.22
require (
github.com/chzyer/test v1.0.0

View File

@ -46,6 +46,8 @@ const (
CharO = 79
CharEscapeEx = 91
CharBackspace = 127
CharK = 107
CharJ = 106
)
const (