mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
499 lines
14 KiB
Go
499 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()
|
|
}
|