VictoriaMetrics/vendor/github.com/ergochat/readline/complete.go

500 lines
14 KiB
Go

package readline
import (
"bufio"
"bytes"
"fmt"
"sync/atomic"
"github.com/ergochat/readline/internal/platform"
"github.com/ergochat/readline/internal/runes"
)
type AutoCompleter interface {
// Readline will pass the whole line and current offset to it
// Completer need to pass all the candidates, and how long they shared the same characters in line
// Example:
// [go, git, git-shell, grep]
// Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
// Do("gi", 2) => ["t", "t-shell"], 2
// Do("git", 3) => ["", "-shell"], 3
Do(line []rune, pos int) (newLine [][]rune, length int)
}
type opCompleter struct {
w *terminal
op *operation
inCompleteMode atomic.Uint32 // this is read asynchronously from wrapWriter
inSelectMode bool
candidate [][]rune // list of candidates
candidateSource []rune // buffer string when tab was pressed
candidateOff int // num runes in common from buf where candidate start
candidateChoice int // absolute index of the chosen candidate (indexing the candidate array which might not all display in current page)
candidateColNum int // num columns candidates take 0..wraps, 1 col, 2 cols etc.
candidateColWidth int // width of candidate columns
linesAvail int // number of lines available below the user's prompt which could be used for rendering the completion
pageStartIdx []int // start index in the candidate array on each page (candidatePageStart[i] = absolute idx of the first candidate on page i)
curPage int // index of the current page
}
func newOpCompleter(w *terminal, op *operation) *opCompleter {
return &opCompleter{
w: w,
op: op,
}
}
func (o *opCompleter) doSelect() {
if len(o.candidate) == 1 {
o.op.buf.WriteRunes(o.candidate[0])
o.ExitCompleteMode(false)
return
}
o.nextCandidate()
o.CompleteRefresh()
}
// Convert absolute index of the chosen candidate to a page-relative index
func (o *opCompleter) candidateChoiceWithinPage() int {
return o.candidateChoice - o.pageStartIdx[o.curPage]
}
// Given a page relative index of the chosen candidate, update the absolute index
func (o *opCompleter) updateAbsolutechoice(choiceWithinPage int) {
o.candidateChoice = choiceWithinPage + o.pageStartIdx[o.curPage]
}
// Move selection to the next candidate, updating page if necessary
// Note: we don't allow passing arbitrary offset to this function because, e.g.,
// we don't have the 3rd page offset initialized when the user is just seeing the first page,
// so we only allow users to navigate into the 2nd page but not to an arbirary page as a result
// of calling this method
func (o *opCompleter) nextCandidate() {
o.candidateChoice = (o.candidateChoice + 1) % len(o.candidate)
// Wrapping around
if o.candidateChoice == 0 {
o.curPage = 0
return
}
// Going to next page
if o.candidateChoice == o.pageStartIdx[o.curPage+1] {
o.curPage += 1
}
}
// Move selection to the next ith col in the current line, wrapping to the line start/end if needed
func (o *opCompleter) nextCol(i int) {
// If o.candidateColNum == 1 or 0, there is only one col per line and this is a noop
if o.candidateColNum > 1 {
idxWithinPage := o.candidateChoiceWithinPage()
curLine := idxWithinPage / o.candidateColNum
offsetInLine := idxWithinPage % o.candidateColNum
nextOffset := offsetInLine + i
nextOffset %= o.candidateColNum
if nextOffset < 0 {
nextOffset += o.candidateColNum
}
nextIdxWithinPage := curLine*o.candidateColNum + nextOffset
o.updateAbsolutechoice(nextIdxWithinPage)
}
}
// Move selection to the line below
func (o *opCompleter) nextLine() {
colNum := 1
if o.candidateColNum > 1 {
colNum = o.candidateColNum
}
idxWithinPage := o.candidateChoiceWithinPage()
idxWithinPage += colNum
if idxWithinPage >= o.getMatrixSize() {
idxWithinPage -= o.getMatrixSize()
} else if idxWithinPage >= o.numCandidateCurPage() {
idxWithinPage += colNum
idxWithinPage -= o.getMatrixSize()
}
o.updateAbsolutechoice(idxWithinPage)
}
// Move selection to the line above
func (o *opCompleter) prevLine() {
colNum := 1
if o.candidateColNum > 1 {
colNum = o.candidateColNum
}
idxWithinPage := o.candidateChoiceWithinPage()
idxWithinPage -= colNum
if idxWithinPage < 0 {
idxWithinPage += o.getMatrixSize()
if idxWithinPage >= o.numCandidateCurPage() {
idxWithinPage -= colNum
}
}
o.updateAbsolutechoice(idxWithinPage)
}
// Move selection to the start of the current line
func (o *opCompleter) lineStart() {
if o.candidateColNum > 1 {
idxWithinPage := o.candidateChoiceWithinPage()
lineOffset := idxWithinPage % o.candidateColNum
idxWithinPage -= lineOffset
o.updateAbsolutechoice(idxWithinPage)
}
}
// Move selection to the end of the current line
func (o *opCompleter) lineEnd() {
if o.candidateColNum > 1 {
idxWithinPage := o.candidateChoiceWithinPage()
offsetToLineEnd := o.candidateColNum - idxWithinPage%o.candidateColNum - 1
idxWithinPage += offsetToLineEnd
o.updateAbsolutechoice(idxWithinPage)
if o.candidateChoice >= len(o.candidate) {
o.candidateChoice = len(o.candidate) - 1
}
}
}
// Move to the next page if possible, returning selection to the first item in the page
func (o *opCompleter) nextPage() {
// Check that this is not the last page already
nextPageStart := o.pageStartIdx[o.curPage+1]
if nextPageStart < len(o.candidate) {
o.curPage += 1
o.candidateChoice = o.pageStartIdx[o.curPage]
}
}
// Move to the previous page if possible, returning selection to the first item in the page
func (o *opCompleter) prevPage() {
if o.curPage > 0 {
o.curPage -= 1
o.candidateChoice = o.pageStartIdx[o.curPage]
}
}
// OnComplete returns true if complete mode is available. Used to ring bell
// when tab pressed if cannot do complete for reason such as width unknown
// or no candidates available.
func (o *opCompleter) OnComplete() (ringBell bool) {
tWidth, tHeight := o.w.GetWidthHeight()
if tWidth == 0 || tHeight < 3 {
return false
}
if o.IsInCompleteSelectMode() {
o.doSelect()
return true
}
buf := o.op.buf
rs := buf.Runes()
// If in complete mode and nothing else typed then we must be entering select mode
if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
if len(o.candidate) > 1 {
same, size := runes.Aggregate(o.candidate)
if size > 0 {
buf.WriteRunes(same)
o.ExitCompleteMode(false)
return false // partial completion so ring the bell
}
}
o.EnterCompleteSelectMode()
o.doSelect()
return true
}
newLines, offset := o.op.GetConfig().AutoComplete.Do(rs, buf.idx)
if len(newLines) == 0 || (len(newLines) == 1 && len(newLines[0]) == 0) {
o.ExitCompleteMode(false)
return false // will ring bell on initial tab press
}
if o.candidateOff > offset {
// part of buffer we are completing has changed. Example might be that we were completing "ls" and
// user typed space so we are no longer completing "ls" but now we are completing an argument of
// the ls command. Instead of continuing in complete mode, we exit.
o.ExitCompleteMode(false)
return true
}
o.candidateSource = rs
// only Aggregate candidates in non-complete mode
if !o.IsInCompleteMode() {
if len(newLines) == 1 {
// not yet in complete mode but only 1 candidate so complete it
buf.WriteRunes(newLines[0])
o.ExitCompleteMode(false)
return true
}
// check if all candidates have common prefix and return it and its size
same, size := runes.Aggregate(newLines)
if size > 0 {
buf.WriteRunes(same)
o.ExitCompleteMode(false)
return false // partial completion so ring the bell
}
}
// otherwise, we just enter complete mode (which does a refresh)
o.EnterCompleteMode(offset, newLines)
return true
}
func (o *opCompleter) IsInCompleteSelectMode() bool {
return o.inSelectMode
}
func (o *opCompleter) IsInCompleteMode() bool {
return o.inCompleteMode.Load() == 1
}
func (o *opCompleter) HandleCompleteSelect(r rune) (stayInMode bool) {
next := true
switch r {
case CharEnter, CharCtrlJ:
next = false
o.op.buf.WriteRunes(o.candidate[o.candidateChoice])
o.ExitCompleteMode(false)
case CharLineStart:
o.lineStart()
case CharLineEnd:
o.lineEnd()
case CharBackspace:
o.ExitCompleteSelectMode()
next = false
case CharTab:
o.nextCandidate()
case CharForward:
o.nextCol(1)
case CharBell, CharInterrupt:
o.ExitCompleteMode(true)
next = false
case CharNext:
o.nextLine()
case CharBackward, MetaShiftTab:
o.nextCol(-1)
case CharPrev:
o.prevLine()
case 'j', 'J':
o.prevPage()
case 'k', 'K':
o.nextPage()
default:
next = false
o.ExitCompleteSelectMode()
}
if next {
o.CompleteRefresh()
return true
}
return false
}
func (o *opCompleter) getMatrixSize() int {
colNum := 1
if o.candidateColNum > 1 {
colNum = o.candidateColNum
}
line := o.getMatrixNumRows()
return line * colNum
}
// Number of candidate that could fit on current page
func (o *opCompleter) numCandidateCurPage() int {
// Safety: we will always render the first page, and whenever we finished rendering page i,
// we always populate o.candidatePageStart through at least i + 1, so when this is called, we
// always know the start of the next page
return o.pageStartIdx[o.curPage+1] - o.pageStartIdx[o.curPage]
}
// Get number of rows of current page viewed as a matrix of candidates
func (o *opCompleter) getMatrixNumRows() int {
candidateCurPage := o.numCandidateCurPage()
// Normal case where there is no wrap
if o.candidateColNum > 1 {
numLine := candidateCurPage / o.candidateColNum
if candidateCurPage%o.candidateColNum != 0 {
numLine++
}
return numLine
}
// Now since there are wraps, each candidate will be put on its own line, so the number of lines is just the number of candidate
return candidateCurPage
}
// setColumnInfo calculates column width and number of columns required
// to present the list of candidates on the terminal.
func (o *opCompleter) setColumnInfo() {
same := o.op.buf.RuneSlice(-o.candidateOff)
sameWidth := runes.WidthAll(same)
colWidth := 0
for _, c := range o.candidate {
w := sameWidth + runes.WidthAll(c)
if w > colWidth {
colWidth = w
}
}
colWidth++ // whitespace between cols
tWidth, _ := o.w.GetWidthHeight()
// -1 to avoid end of line issues
width := tWidth - 1
colNum := width / colWidth
if colNum != 0 {
colWidth += (width - (colWidth * colNum)) / colNum
}
o.candidateColNum = colNum
o.candidateColWidth = colWidth
}
// CompleteRefresh is used for completemode and selectmode
func (o *opCompleter) CompleteRefresh() {
if !o.IsInCompleteMode() {
return
}
buf := bufio.NewWriter(o.w)
// calculate num lines from cursor pos to where choices should be written
lineCnt := o.op.buf.CursorLineCount()
buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) // move down from cursor to start of candidates
buf.WriteString("\033[J")
same := o.op.buf.RuneSlice(-o.candidateOff)
tWidth, _ := o.w.GetWidthHeight()
colIdx := 0
lines := 0
sameWidth := runes.WidthAll(same)
// Show completions for the current page
idx := o.pageStartIdx[o.curPage]
for ; idx < len(o.candidate); idx++ {
// If writing the current candidate would overflow the page,
// we know that it is the start of the next page.
if colIdx == 0 && lines == o.linesAvail {
if o.curPage == len(o.pageStartIdx)-1 {
o.pageStartIdx = append(o.pageStartIdx, idx)
}
break
}
c := o.candidate[idx]
inSelect := idx == o.candidateChoice && o.IsInCompleteSelectMode()
cWidth := sameWidth + runes.WidthAll(c)
cLines := 1
if tWidth > 0 {
sWidth := 0
if platform.IsWindows && inSelect {
sWidth = 1 // adjust for hightlighting on Windows
}
cLines = (cWidth + sWidth) / tWidth
if (cWidth+sWidth)%tWidth > 0 {
cLines++
}
}
if lines > 0 && colIdx == 0 {
// After line 1, if we're printing to the first column
// goto a new line. We do it here, instead of at the end
// of the loop, to avoid the last \n taking up a blank
// line at the end and stealing realestate.
buf.WriteString("\n")
}
if inSelect {
buf.WriteString("\033[30;47m")
}
buf.WriteString(string(same))
buf.WriteString(string(c))
if o.candidateColNum >= 1 {
// only output spaces between columns if everything fits
buf.Write(bytes.Repeat([]byte(" "), o.candidateColWidth-cWidth))
}
if inSelect {
buf.WriteString("\033[0m")
}
colIdx++
if colIdx >= o.candidateColNum {
lines += cLines
colIdx = 0
if platform.IsWindows {
// Windows EOL edge-case.
buf.WriteString("\b")
}
}
}
if idx == len(o.candidate) {
// Book-keeping for the last page.
o.pageStartIdx = append(o.pageStartIdx, len(o.candidate))
}
if colIdx > 0 {
lines++ // mid-line so count it.
}
// Show the guidance if there are more pages
if idx != len(o.candidate) || o.curPage > 0 {
buf.WriteString("\n-- (j: prev page) (k: next page) --")
lines++
}
// wrote out choices over "lines", move back to cursor (positioned at index)
fmt.Fprintf(buf, "\033[%dA", lines)
buf.Write(o.op.buf.getBackspaceSequence())
buf.Flush()
}
func (o *opCompleter) EnterCompleteSelectMode() {
o.inSelectMode = true
o.candidateChoice = -1
}
func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
o.inCompleteMode.Store(1)
o.candidate = candidate
o.candidateOff = offset
o.setColumnInfo()
o.initPage()
o.CompleteRefresh()
}
func (o *opCompleter) initPage() {
_, tHeight := o.w.GetWidthHeight()
buflineCnt := o.op.buf.LineCount() // lines taken by buffer content
o.linesAvail = tHeight - buflineCnt - 1 // lines available without scrolling buffer off screen, reserve one line for the guidance message
o.pageStartIdx = []int{0} // first page always start at 0
o.curPage = 0
}
func (o *opCompleter) ExitCompleteSelectMode() {
o.inSelectMode = false
o.candidateChoice = -1
}
func (o *opCompleter) ExitCompleteMode(revent bool) {
o.inCompleteMode.Store(0)
o.candidate = nil
o.candidateOff = -1
o.candidateSource = nil
o.ExitCompleteSelectMode()
}