2022-03-13 17:48:49 +00:00
|
|
|
package readline
|
|
|
|
|
|
|
|
import (
|
2023-01-21 18:18:45 +00:00
|
|
|
"regexp"
|
2022-03-13 17:48:49 +00:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// When the DelayedSyntaxWorker gives us a new line, we need to check if there
|
|
|
|
// is any processing to be made, that all lines match in terms of content.
|
|
|
|
func (rl *Instance) updateLine(line []rune) {
|
|
|
|
if len(rl.currentComp) > 0 {
|
|
|
|
|
|
|
|
} else {
|
|
|
|
rl.line = line
|
|
|
|
}
|
|
|
|
|
|
|
|
rl.renderHelpers()
|
|
|
|
}
|
|
|
|
|
|
|
|
// getLine - In many places we need the current line input. We either return the real line,
|
|
|
|
// or the one that includes the current completion candidate, if there is any.
|
2022-07-19 21:55:03 +00:00
|
|
|
func (rl *Instance) GetLine() []rune {
|
2022-03-13 17:48:49 +00:00
|
|
|
if len(rl.currentComp) > 0 {
|
|
|
|
return rl.lineComp
|
|
|
|
}
|
|
|
|
return rl.line
|
|
|
|
}
|
|
|
|
|
2023-01-21 18:18:45 +00:00
|
|
|
func (rl *Instance) lineSuggested() (line []rune, cpos int) {
|
|
|
|
//rl.checkCursorBounds()
|
|
|
|
|
|
|
|
if len(rl.currentComp) > 0 {
|
|
|
|
line = rl.lineComp
|
|
|
|
cpos = len(rl.lineComp[:rl.pos])
|
|
|
|
} else {
|
|
|
|
line = rl.line
|
|
|
|
cpos = len(rl.line[:rl.pos])
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
if len(rl.histSuggested) > 0 {
|
|
|
|
line = append(line, rl.histSuggested...)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
return line, cpos
|
|
|
|
}
|
|
|
|
|
|
|
|
// computeLinePos determines the X and Y coordinates of the cursor.
|
|
|
|
func (rl *Instance) computeLinePos() {
|
|
|
|
// Use the line including any completion or line suggestion,
|
|
|
|
// and compute buffer/cursor length. Only add a newline when
|
|
|
|
// the current buffer does not end with one.
|
|
|
|
line, cpos := rl.lineSuggested()
|
|
|
|
line = append(line, '\n')
|
|
|
|
|
|
|
|
// Get the index of each newline in the buffer.
|
|
|
|
nl := regexp.MustCompile("\n")
|
|
|
|
newlinesIdx := nl.FindAllStringIndex(string(line), -1)
|
|
|
|
|
|
|
|
rl.posY = 0
|
|
|
|
rl.fullY = 0
|
|
|
|
startLine := 0
|
|
|
|
cursorSet := false
|
|
|
|
|
|
|
|
for pos, newline := range newlinesIdx {
|
|
|
|
// Compute any adjustment in case this line must be wrapped.
|
|
|
|
// Here, compute if line must be wrapped, to adjust posY.
|
|
|
|
lineY := rl.realLineLen(line[startLine:newline[0]], pos)
|
|
|
|
|
|
|
|
// All lines add to the global offset.
|
|
|
|
rl.fullY += lineY
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case newline[0] < cpos:
|
|
|
|
// If we are not on the cursor line yet.
|
|
|
|
rl.posY += lineY
|
|
|
|
case !cursorSet:
|
|
|
|
// We are on the cursor line, since we didn't catch
|
|
|
|
// the first case, and that our cursor X coordinate
|
|
|
|
// has not been set yet.
|
|
|
|
rl.computeCursorPos(startLine, cpos, pos)
|
|
|
|
cursorSet = true
|
|
|
|
rl.hpos = pos
|
|
|
|
}
|
|
|
|
|
|
|
|
startLine = newline[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// computeCursorPos computes the X/Y coordinates of the cursor on a given line.
|
|
|
|
func (rl *Instance) computeCursorPos(startLine, cpos, lineIdx int) {
|
|
|
|
termWidth := GetTermWidth()
|
|
|
|
cursorStart := cpos - startLine
|
2023-01-30 00:57:34 +00:00
|
|
|
cursorStart += rl.getPromptPos()
|
2023-01-21 18:18:45 +00:00
|
|
|
|
|
|
|
cursorY := cursorStart / termWidth
|
|
|
|
cursorX := cursorStart % termWidth
|
|
|
|
|
|
|
|
// The very first (unreal) line counts for nothing,
|
|
|
|
// so by opposition all others count for one more.
|
|
|
|
if lineIdx == 0 {
|
|
|
|
cursorY--
|
|
|
|
}
|
|
|
|
|
|
|
|
// Any excess wrap means a newline.
|
|
|
|
if cursorX > 0 {
|
|
|
|
cursorY++
|
|
|
|
}
|
|
|
|
|
|
|
|
rl.posY += cursorY
|
|
|
|
rl.posX = cursorX
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rl *Instance) realLineLen(line []rune, idx int) (lineY int) {
|
|
|
|
lineLen := getRealLength(string(line))
|
|
|
|
termWidth := GetTermWidth()
|
|
|
|
//lineLen += rl.Prompt.inputAt(rl)
|
|
|
|
|
|
|
|
lineY = lineLen / termWidth
|
|
|
|
restY := lineLen % termWidth
|
|
|
|
|
|
|
|
// The very first line counts for nothing.
|
|
|
|
if idx == 0 {
|
|
|
|
lineY--
|
|
|
|
}
|
|
|
|
|
|
|
|
// Any excess wrap means a newline.
|
|
|
|
if restY > 0 {
|
|
|
|
lineY++
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty lines are still considered a line.
|
|
|
|
if lineY == 0 && idx != 0 {
|
|
|
|
lineY++
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-13 17:48:49 +00:00
|
|
|
// echo - refresh the current input line, either virtually completed or not.
|
|
|
|
// also renders the current completions and hints. To be noted, the updateReferences()
|
|
|
|
// function is only ever called once, and after having moved back to prompt position
|
|
|
|
// and having printed the line: this is so that at any moment, everyone has the good
|
|
|
|
// values for moving around, synchronized with the update input line.
|
|
|
|
func (rl *Instance) echo() {
|
|
|
|
|
|
|
|
// Then we print the prompt, and the line,
|
|
|
|
switch {
|
|
|
|
case rl.PasswordMask != 0:
|
|
|
|
case rl.PasswordMask > 0:
|
|
|
|
print(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ")
|
|
|
|
|
|
|
|
default:
|
|
|
|
// Go back to prompt position, and clear everything below
|
|
|
|
moveCursorBackwards(GetTermWidth())
|
|
|
|
moveCursorUp(rl.posY)
|
|
|
|
|
|
|
|
// Print the prompt
|
|
|
|
print(string(rl.realPrompt))
|
|
|
|
|
2023-01-30 00:57:34 +00:00
|
|
|
// print the line
|
|
|
|
rl.printBuffer()
|
2023-01-21 18:18:45 +00:00
|
|
|
|
2023-01-30 00:57:34 +00:00
|
|
|
// update cursor positions
|
2023-01-21 18:18:45 +00:00
|
|
|
rl.computeLinePos()
|
2022-03-13 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update references with new coordinates only now, because
|
|
|
|
// the new line may be longer/shorter than the previous one.
|
2023-01-21 18:18:45 +00:00
|
|
|
//rl.updateReferences()
|
2022-03-13 17:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (rl *Instance) insert(r []rune) {
|
|
|
|
for {
|
|
|
|
// I don't really understand why `0` is creaping in at the end of the
|
|
|
|
// array but it only happens with unicode characters.
|
|
|
|
if len(r) > 1 && r[len(r)-1] == 0 {
|
|
|
|
r = r[:len(r)-1]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// We can ONLY have three fondamentally different cases:
|
|
|
|
switch {
|
|
|
|
// The line is empty
|
|
|
|
case len(rl.line) == 0:
|
|
|
|
rl.line = r
|
|
|
|
|
|
|
|
// We are inserting somewhere in the middle
|
|
|
|
case rl.pos < len(rl.line):
|
|
|
|
r := append(r, rl.line[rl.pos:]...)
|
|
|
|
rl.line = append(rl.line[:rl.pos], r...)
|
|
|
|
|
|
|
|
// We are at the end of the input line
|
|
|
|
case rl.pos == len(rl.line):
|
|
|
|
rl.line = append(rl.line, r...)
|
|
|
|
}
|
|
|
|
|
|
|
|
rl.pos += len(r)
|
|
|
|
|
|
|
|
// This should also update the rl.pos
|
|
|
|
rl.updateHelpers()
|
|
|
|
}
|
|
|
|
|
2023-01-21 18:18:45 +00:00
|
|
|
// lineSlice returns a subset of the current input line.
|
|
|
|
func (rl *Instance) lineSlice(adjust int) (slice string) {
|
|
|
|
switch {
|
|
|
|
case rl.pos+adjust > len(rl.line):
|
|
|
|
slice = string(rl.line[rl.pos:])
|
|
|
|
case adjust < 0:
|
|
|
|
if rl.pos+adjust < 0 {
|
|
|
|
slice = string(rl.line[:rl.pos])
|
|
|
|
} else {
|
|
|
|
slice = string(rl.line[rl.pos+adjust : rl.pos])
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
slice = string(rl.line[rl.pos : rl.pos+adjust])
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rl *Instance) lineCarriageReturn() {
|
|
|
|
//rl.histSuggested = []rune{}
|
|
|
|
|
|
|
|
// Ask the caller if the line should be accepted as is.
|
|
|
|
if rl.AcceptMultiline(rl.GetLine()) {
|
|
|
|
// Clear the tooltip prompt if any,
|
|
|
|
// then go down and clear hints/completions.
|
|
|
|
//rl.moveToLineEnd()
|
|
|
|
//rl.Prompt.clearRprompt(rl, false)
|
|
|
|
print("\r\n")
|
|
|
|
print(seqClearScreenBelow)
|
|
|
|
|
|
|
|
// Save the command line and accept it.
|
|
|
|
//rl.writeHistoryLine()
|
|
|
|
rl.accepted = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not, we should start editing another line,
|
|
|
|
// and insert a newline where our cursor value is.
|
|
|
|
// This has the nice advantage of being able to work
|
|
|
|
// in multiline mode even in the middle of the buffer.
|
|
|
|
rl.insert([]rune{'\n'})
|
|
|
|
}
|
|
|
|
|
2022-05-14 00:43:40 +00:00
|
|
|
func (rl *Instance) Insert(t string) {
|
|
|
|
rl.insert([]rune(t))
|
|
|
|
}
|
|
|
|
|
2022-03-13 17:48:49 +00:00
|
|
|
func (rl *Instance) deleteX() {
|
|
|
|
switch {
|
|
|
|
case len(rl.line) == 0:
|
|
|
|
return
|
|
|
|
case rl.pos == 0:
|
|
|
|
rl.line = rl.line[1:]
|
|
|
|
case rl.pos > len(rl.line):
|
|
|
|
rl.pos = len(rl.line)
|
|
|
|
case rl.pos == len(rl.line):
|
|
|
|
rl.pos--
|
|
|
|
rl.line = rl.line[:rl.pos]
|
|
|
|
default:
|
|
|
|
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
rl.updateHelpers()
|
|
|
|
}
|
|
|
|
|
2022-03-24 01:11:24 +00:00
|
|
|
func (rl *Instance) deleteBackspace(forward bool) {
|
2022-03-13 17:48:49 +00:00
|
|
|
switch {
|
|
|
|
case len(rl.line) == 0:
|
|
|
|
return
|
2022-03-24 01:11:24 +00:00
|
|
|
case forward:
|
|
|
|
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
|
2022-03-13 17:48:49 +00:00
|
|
|
case rl.pos > len(rl.line):
|
2022-03-24 01:11:24 +00:00
|
|
|
rl.backspace(forward) // There is an infite loop going on here...
|
2022-03-13 17:48:49 +00:00
|
|
|
case rl.pos == len(rl.line):
|
|
|
|
rl.pos--
|
|
|
|
rl.line = rl.line[:rl.pos]
|
|
|
|
default:
|
|
|
|
rl.pos--
|
|
|
|
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
rl.updateHelpers()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rl *Instance) clearLine() {
|
|
|
|
if len(rl.line) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to go back to prompt
|
|
|
|
moveCursorUp(rl.posY)
|
|
|
|
moveCursorBackwards(GetTermWidth())
|
|
|
|
moveCursorForwards(rl.promptLen)
|
|
|
|
|
|
|
|
// Clear everything after & below the cursor
|
|
|
|
print(seqClearScreenBelow)
|
|
|
|
|
|
|
|
// Real input line
|
|
|
|
rl.line = []rune{}
|
|
|
|
rl.lineComp = []rune{}
|
|
|
|
rl.pos = 0
|
|
|
|
rl.posX = 0
|
|
|
|
rl.fullX = 0
|
|
|
|
rl.posY = 0
|
|
|
|
rl.fullY = 0
|
|
|
|
|
|
|
|
// Completions are also reset
|
|
|
|
rl.clearVirtualComp()
|
|
|
|
}
|
|
|
|
|
2023-01-30 00:57:34 +00:00
|
|
|
func (rl *Instance) printBuffer() {
|
|
|
|
// Generate the entire line as an highlighted line,
|
|
|
|
// and split it at each newline.
|
|
|
|
line := string(rl.GetLine())
|
|
|
|
lines := strings.Split(line, "\n")
|
|
|
|
|
|
|
|
if len(line) > 0 && line[len(line)-1] == '\n' {
|
|
|
|
lines = append(lines, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, line := range lines {
|
|
|
|
// Indent according to the prompt.
|
|
|
|
if i > 0 {
|
|
|
|
moveCursorForwards(rl.getPromptPos())
|
|
|
|
}
|
|
|
|
|
|
|
|
if i < len(lines)-1 {
|
|
|
|
line += "\n"
|
|
|
|
} else {
|
|
|
|
line += seqClearScreenBelow
|
|
|
|
}
|
|
|
|
|
|
|
|
print(line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-13 17:48:49 +00:00
|
|
|
func (rl *Instance) deleteToBeginning() {
|
|
|
|
rl.resetVirtualComp(false)
|
|
|
|
// Keep the line length up until the cursor
|
|
|
|
rl.line = rl.line[rl.pos:]
|
|
|
|
rl.pos = 0
|
|
|
|
}
|
2022-03-27 03:43:30 +00:00
|
|
|
|
|
|
|
func (rl *Instance) deleteToEnd() {
|
|
|
|
rl.resetVirtualComp(false)
|
|
|
|
// Keep everything before the cursor
|
|
|
|
rl.line = rl.line[:rl.pos]
|
|
|
|
}
|
2022-04-13 03:08:44 +00:00
|
|
|
|
2022-04-14 11:42:46 +00:00
|
|
|
// @TODO(Renzix): move to emacs sepecific file
|
2022-04-13 03:08:44 +00:00
|
|
|
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
|
|
|
|
split, index, pos := tokeniser(rl.line, rl.pos)
|
|
|
|
if len(split) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-14 11:42:46 +00:00
|
|
|
word := strings.TrimSpace(split[index])
|
2022-04-13 03:08:44 +00:00
|
|
|
|
|
|
|
switch {
|
|
|
|
case len(split) == 0:
|
|
|
|
return
|
2022-04-14 11:42:46 +00:00
|
|
|
case pos == len(word) && index != len(split)-1:
|
|
|
|
extrawhitespace := len(strings.TrimLeft(split[index], " ")) - len(word)
|
|
|
|
word = split[index+1]
|
|
|
|
adjust = len(word) + extrawhitespace
|
2022-04-13 03:08:44 +00:00
|
|
|
default:
|
|
|
|
adjust = len(word) - pos
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2022-04-14 11:42:46 +00:00
|
|
|
|
|
|
|
func (rl *Instance) emacsBackwardWord(tokeniser tokeniser) (adjust int) {
|
|
|
|
split, index, pos := tokeniser(rl.line, rl.pos)
|
|
|
|
if len(split) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case len(split) == 0:
|
|
|
|
return
|
|
|
|
case pos == 0 && index != 0:
|
|
|
|
adjust = len(split[index-1])
|
|
|
|
default:
|
|
|
|
adjust = pos
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|