diff --git a/complete.go b/complete.go index c08c994..34064ee 100644 --- a/complete.go +++ b/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() } diff --git a/example/readline-paged-completion/readline-paged-completion.go b/example/readline-paged-completion/readline-paged-completion.go new file mode 100644 index 0000000..fc7427c --- /dev/null +++ b/example/readline-paged-completion/readline-paged-completion.go @@ -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)) + } + } +} diff --git a/go.mod b/go.mod index 66180f6..223513f 100644 --- a/go.mod +++ b/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 diff --git a/utils.go b/utils.go index 0706dd4..c2179aa 100644 --- a/utils.go +++ b/utils.go @@ -46,6 +46,8 @@ const ( CharO = 79 CharEscapeEx = 91 CharBackspace = 127 + CharK = 107 + CharJ = 106 ) const (