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
|
||||
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 {
|
||||
return &opCompleter{
|
||||
w: w,
|
||||
op: op,
|
||||
width: width,
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
go 1.15
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/chzyer/test v1.0.0
|
||||
|
|
Loading…
Reference in New Issue