Hilbish/readline/line.go

394 lines
8.8 KiB
Go

package readline
import (
"regexp"
"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.
func (rl *Instance) GetLine() []rune {
if len(rl.currentComp) > 0 {
return rl.lineComp
}
return rl.line
}
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
cursorStart += rl.getPromptPos()
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
}
// 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))
// print the line
rl.printBuffer()
// update cursor positions
rl.computeLinePos()
}
// Update references with new coordinates only now, because
// the new line may be longer/shorter than the previous one.
//rl.updateReferences()
}
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()
}
// 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'})
}
func (rl *Instance) Insert(t string) {
rl.insert([]rune(t))
}
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()
}
func (rl *Instance) deleteBackspace(forward bool) {
switch {
case len(rl.line) == 0:
return
case forward:
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
case rl.pos > len(rl.line):
rl.backspace(forward) // There is an infite loop going on here...
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()
}
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)
}
}
func (rl *Instance) deleteToBeginning() {
rl.resetVirtualComp(false)
// Keep the line length up until the cursor
rl.line = rl.line[rl.pos:]
rl.pos = 0
}
func (rl *Instance) deleteToEnd() {
rl.resetVirtualComp(false)
// Keep everything before the cursor
rl.line = rl.line[:rl.pos]
}
// @TODO(Renzix): move to emacs sepecific file
func (rl *Instance) emacsForwardWord(tokeniser tokeniser) (adjust int) {
split, index, pos := tokeniser(rl.line, rl.pos)
if len(split) == 0 {
return
}
word := strings.TrimSpace(split[index])
switch {
case len(split) == 0:
return
case pos == len(word) && index != len(split)-1:
extrawhitespace := len(strings.TrimLeft(split[index], " ")) - len(word)
word = split[index+1]
adjust = len(word) + extrawhitespace
default:
adjust = len(word) - pos
}
return
}
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
}