diff --git a/readline/instance.go b/readline/instance.go index 039f040..dddf4e0 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -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() diff --git a/readline/line.go b/readline/line.go index 2024bb0..e9905f5 100644 --- a/readline/line.go +++ b/readline/line.go @@ -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)) } diff --git a/readline/readline.go b/readline/readline.go index f1d6c96..e650dfa 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -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() - return string(rl.line), nil + rl.lineCarriageReturn() + if rl.accepted { + return string(rl.line), nil + } // Vim -------------------------------------------------------------------------------------- case charEscape: diff --git a/readline/vim.go b/readline/vim.go index d496705..1c44948 100644 --- a/readline/vim.go +++ b/readline/vim.go @@ -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