feat: initial multiline support

multiline
sammyette 2023-01-21 14:18:45 -04:00
parent f97a04179d
commit d22428bd08
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
4 changed files with 182 additions and 66 deletions

View File

@ -47,10 +47,12 @@ type Instance struct {
// readline operating parameters
line []rune // This is the input line, with entered text: full line = mlnPrompt + line
accepted bool // Set by 'accept-line' widget, to notify return the line to the caller
pos int
hpos int // The line on which the cursor is (differs from posY, which accounts for wraps)
posX int // Cursor position X
fullX int // X coordinate of the full input line, including the prompt if needed.
posY int // Cursor position Y (if multiple lines span)
fullX int // X coordinate of the full input line, including the prompt if needed.
fullY int // Y offset to the end of input line.
// Buffer received from host programms
@ -183,11 +185,12 @@ type Instance struct {
// $EDITOR. This will default to os.TempDir()
TempDirectory string
// GetMultiLine is a callback to your host program. Since multiline support
// is handled by the application rather than readline itself, this callback
// is required when calling $EDITOR. However if this function is not set
// then readline will just use the current line.
GetMultiLine func([]rune) []rune
// AcceptMultiline enables the caller to decide if the shell should keep reading for user input
// on a new line (therefore, with the secondary prompt), or if it should return the current
// line at the end of the `rl.Readline()` call.
// This function should return "true" if the line is deemed complete (thus asking the shell
// to return from its Readline() loop), or "false" if the shell should keep reading input.
AcceptMultiline func([]rune) bool
EnableGetCursorPos bool
@ -229,6 +232,9 @@ func NewInstance() *Instance {
rl.HintFormatting = "\x1b[2m"
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
rl.TempDirectory = os.TempDir()
rl.AcceptMultiline = func([]rune) bool {
return false
}
// Registers
rl.initRegisters()

View File

@ -1,6 +1,7 @@
package readline
import (
"regexp"
"strings"
)
@ -25,6 +26,118 @@ func (rl *Instance) GetLine() []rune {
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.Prompt.inputAt(rl)
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
@ -42,7 +155,6 @@ func (rl *Instance) echo() {
// Go back to prompt position, and clear everything below
moveCursorBackwards(GetTermWidth())
moveCursorUp(rl.posY)
print(seqClearScreenBelow)
// Print the prompt
print(string(rl.realPrompt))
@ -55,23 +167,19 @@ func (rl *Instance) echo() {
line = rl.line
}
printed := string(line)
// Print the input line with optional syntax highlighting
if rl.SyntaxHighlighter != nil {
print(rl.SyntaxHighlighter(line))
} else {
print(string(line))
printed = (rl.SyntaxHighlighter(line))
}
rl.computeLinePos()
print(printed)
}
// Update references with new coordinates only now, because
// the new line may be longer/shorter than the previous one.
rl.updateReferences()
// Go back to the current cursor position, with new coordinates
moveCursorBackwards(GetTermWidth())
moveCursorUp(rl.fullY)
moveCursorDown(rl.posY)
moveCursorForwards(rl.posX)
//rl.updateReferences()
}
func (rl *Instance) insert(r []rune) {
@ -107,6 +215,49 @@ func (rl *Instance) insert(r []rune) {
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))
}

View File

@ -52,22 +52,6 @@ func (rl *Instance) Readline() (string, error) {
rl.histOffset = 0
rl.viUndoHistory = []undoItem{{line: "", pos: 0}}
// Multisplit
if len(rl.multisplit) > 0 {
r := []rune(rl.multisplit[0])
if len(r) >= 1 {
rl.editorInput(r)
}
rl.carridgeReturn()
if len(rl.multisplit) > 1 {
rl.multisplit = rl.multisplit[1:]
} else {
rl.multisplit = []string{}
}
return string(rl.line), nil
}
// Finally, print any info or completions
// if the TabCompletion engines so desires
rl.renderHelpers()
@ -98,33 +82,6 @@ func (rl *Instance) Readline() (string, error) {
rl.RawInputCallback(r[:i])
}
if isMultiline(r[:i]) || len(rl.multiline) > 0 {
rl.multiline = append(rl.multiline, b[:i]...)
if i == len(b) {
continue
}
if !rl.allowMultiline(rl.multiline) {
rl.multiline = []byte{}
continue
}
s := string(rl.multiline)
rl.multisplit = rxMultiline.Split(s, -1)
r = []rune(rl.multisplit[0])
rl.modeViMode = VimInsert
rl.editorInput(r)
rl.carridgeReturn()
rl.multiline = []byte{}
if len(rl.multisplit) > 1 {
rl.multisplit = rl.multisplit[1:]
} else {
rl.multisplit = []string{}
}
return string(rl.line), nil
}
s := string(r[:i])
if rl.evtKeyPress[s] != nil {
rl.clearHelpers()
@ -448,7 +405,7 @@ func (rl *Instance) Readline() (string, error) {
completion := cur.getCurrentCell(rl)
prefix := len(rl.tcPrefix)
if prefix > len(completion) {
rl.carridgeReturn()
rl.lineCarriageReturn()
return string(rl.line), nil
}
@ -459,7 +416,7 @@ func (rl *Instance) Readline() (string, error) {
// If we were in history completion, immediately execute the line.
if rl.modeAutoFind && rl.searchMode == HistoryFind {
rl.carridgeReturn()
rl.lineCarriageReturn()
return string(rl.line), nil
}
@ -470,8 +427,10 @@ func (rl *Instance) Readline() (string, error) {
continue
}
rl.carridgeReturn()
rl.lineCarriageReturn()
if rl.accepted {
return string(rl.line), nil
}
// Vim --------------------------------------------------------------------------------------
case charEscape:

View File

@ -238,11 +238,11 @@ func (rl *Instance) vi(r rune) {
case 'v':
rl.clearHelpers()
var multiline []rune
if rl.GetMultiLine == nil {
/*if rl.GetMultiLine == nil {
multiline = rl.line
} else {
multiline = rl.GetMultiLine(rl.line)
}
}*/
// Keep the previous cursor position
//prev := rl.pos