diff --git a/document.go b/document.go index 94eddc02..7cb0f213 100644 --- a/document.go +++ b/document.go @@ -337,6 +337,16 @@ func (d *Document) CursorPositionRow() (row istrings.RuneNumber) { return } +// TextEndPositionRow returns the row of the end of the current text. (0-based.) +func (d *Document) TextEndPositionRow() (row istrings.RuneNumber) { + textLength := istrings.RuneCount(d.Text) + if textLength == 0 { + return 0 + } + row, _ = d.findLineStartIndex(textLength - 1) + return +} + // CursorPositionCol returns the current column. (0-based.) func (d *Document) CursorPositionCol() (col istrings.RuneNumber) { _, index := d.findLineStartIndex(d.cursorPosition) diff --git a/position_test.go b/position_test.go index cc9864ce..ce2ed7f4 100644 --- a/position_test.go +++ b/position_test.go @@ -32,6 +32,14 @@ func TestPositionAtEndOfString(t *testing.T) { Y: 0, }, }, + "one word": { + input: "foo", + columns: 20, + want: Position{ + X: 3, + Y: 0, + }, + }, "one-line fits in columns": { input: "foo bar", columns: 20, @@ -48,6 +56,14 @@ func TestPositionAtEndOfString(t *testing.T) { Y: 2, }, }, + "one-line wrapping": { + input: "foobar", + columns: 3, + want: Position{ + X: 0, + Y: 2, + }, + }, } for name, tc := range tests { diff --git a/prompt.go b/prompt.go index d1fd59ca..1fb6b81d 100644 --- a/prompt.go +++ b/prompt.go @@ -208,8 +208,8 @@ keySwitch: p.buf = NewBuffer() p.history.Clear() case Up, ControlP: - cursor := p.buf.Document().GetCursorPosition(p.renderer.col) - if cursor.Y != 0 { + line := p.buf.Document().CursorPositionRow() + if line > 0 { p.buf.CursorUp(1) break } @@ -222,9 +222,9 @@ keySwitch: } case Down, ControlN: - endOfTextCursor := p.buf.Document().GetEndOfTextPosition(p.renderer.col) - cursor := p.buf.Document().GetCursorPosition(p.renderer.col) - if endOfTextCursor.Y > cursor.Y { + endOfTextRow := p.buf.Document().TextEndPositionRow() + row := p.buf.Document().CursorPositionRow() + if endOfTextRow > row { p.buf.CursorDown(1) break } diff --git a/renderer.go b/renderer.go index 660764e5..35805489 100644 --- a/renderer.go +++ b/renderer.go @@ -1,7 +1,6 @@ package prompt import ( - "runtime" "strings" "unicode/utf8" @@ -126,6 +125,7 @@ func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) return } prefix := r.prefixCallback() + prefixWidth := istrings.GetWidth(prefix) formatted, width := formatSuggestions( suggestions, r.col-istrings.GetWidth(prefix)-1, // -1 means a width of scrollbar @@ -140,7 +140,7 @@ func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] r.prepareArea(windowHeight) - cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), r.col) + cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), r.col-prefixWidth) x := cursor.X if x+width >= r.col { cursor = r.backward(cursor, x+width-r.col) @@ -194,7 +194,6 @@ func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) r.out.SetColor(DefaultColor, DefaultColor, false) c := cursor.Add(Position{X: width}) - r.lineWrap(&c) r.backward(c, width) } @@ -219,7 +218,7 @@ func (r *Renderer) Render(buffer *Buffer, completion *CompletionManager, lexer L text := buffer.Text() prefix := r.prefixCallback() prefixWidth := istrings.GetWidth(prefix) - cursor := positionAtEndOfString(text, r.col) + cursor := positionAtEndOfString(text, r.col-prefixWidth) cursor.X += prefixWidth // prepare area @@ -239,10 +238,9 @@ func (r *Renderer) Render(buffer *Buffer, completion *CompletionManager, lexer L r.out.SetColor(DefaultColor, DefaultColor, false) - r.lineWrap(&cursor) - - targetCursor := buffer.DisplayCursorPosition(r.col) + targetCursor := buffer.DisplayCursorPosition(r.col - prefixWidth) targetCursor.X += prefixWidth + // Log("col: %#v, targetCursor: %#v, cursor: %#v\n", r.col-prefixWidth, targetCursor, cursor) cursor = r.move(cursor, targetCursor) r.renderCompletion(buffer, completion) @@ -263,9 +261,7 @@ func (r *Renderer) Render(buffer *Buffer, completion *CompletionManager, lexer L r.out.SetColor(DefaultColor, DefaultColor, false) - cursor = cursor.Join(positionAtEndOfString(rest, r.col)) - - r.lineWrap(&cursor) + cursor = cursor.Join(positionAtEndOfString(rest, r.col-prefixWidth)) cursor = r.move(cursor, endOfSuggestionPos) } @@ -279,21 +275,32 @@ func (r *Renderer) renderText(lexer Lexer, text string) { } prefix := r.prefixCallback() + prefixWidth := istrings.GetWidth(prefix) + col := r.col - prefixWidth multilinePrefix := r.getMultilinePrefix(prefix) firstIteration := true var lineBuffer strings.Builder + var lineCharIndex istrings.Width + for _, char := range text { - lineBuffer.WriteRune(char) - if char != '\n' { + if lineCharIndex >= col || char == '\n' { + lineBuffer.WriteRune('\n') + r.renderLine(prefix, lineBuffer.String(), r.inputTextColor) + lineCharIndex = 0 + lineBuffer.Reset() + if char != '\n' { + lineBuffer.WriteRune(char) + lineCharIndex += istrings.GetRuneWidth(char) + } + if firstIteration { + prefix = multilinePrefix + firstIteration = false + } continue } - r.renderLine(prefix, lineBuffer.String(), r.inputTextColor) - lineBuffer.Reset() - if firstIteration { - prefix = multilinePrefix - firstIteration = false - } + lineBuffer.WriteRune(char) + lineCharIndex += istrings.GetRuneWidth(char) } r.renderLine(prefix, lineBuffer.String(), r.inputTextColor) @@ -353,8 +360,11 @@ func (r *Renderer) lex(lexer Lexer, input string) { s := input prefix := r.prefixCallback() - r.renderPrefix(prefix) + prefixWidth := istrings.GetWidth(prefix) + col := r.col - prefixWidth multilinePrefix := r.getMultilinePrefix(prefix) + r.renderPrefix(prefix) + var lineCharIndex istrings.Width for { token, ok := lexer.Next() if !ok { @@ -365,15 +375,25 @@ func (r *Renderer) lex(lexer Lexer, input string) { s = strings.TrimPrefix(s, text) var lineBuffer strings.Builder + for _, char := range text { - lineBuffer.WriteRune(char) - if char != '\n' { + if lineCharIndex >= col || char == '\n' { + if char != '\n' { + lineBuffer.WriteByte('\n') + } + r.writeString(lineBuffer.String(), token.Color()) + r.renderPrefix(multilinePrefix) + lineCharIndex = 0 + lineBuffer.Reset() + if char != '\n' { + lineBuffer.WriteRune(char) + lineCharIndex += istrings.GetRuneWidth(char) + } continue } - r.writeString(lineBuffer.String(), token.Color()) - r.renderPrefix(multilinePrefix) - lineBuffer.Reset() + lineBuffer.WriteRune(char) + lineCharIndex += istrings.GetRuneWidth(char) } r.writeString(lineBuffer.String(), token.Color()) } @@ -382,11 +402,17 @@ func (r *Renderer) lex(lexer Lexer, input string) { // BreakLine to break line. func (r *Renderer) BreakLine(buffer *Buffer, lexer Lexer) { // Erasing and Renderer - cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.prefixCallback(), r.col) + prefix := r.prefixCallback() + prefixWidth := istrings.GetWidth(prefix) + cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor(), r.col-prefixWidth) + cursor.X += prefixWidth r.clear(cursor) - text := buffer.Document().Text + "\n" + text := buffer.Document().Text r.renderText(lexer, text) + if _, err := r.out.WriteString("\n"); err != nil { + panic(err) + } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -420,14 +446,6 @@ func (r *Renderer) move(from, to Position) Position { return to } -func (r *Renderer) lineWrap(cursor *Position) { - if runtime.GOOS != "windows" && cursor.X > 0 && cursor.X%r.col == 0 { - cursor.X = 0 - cursor.Y += 1 - r.out.WriteRaw([]byte{'\n'}) - } -} - func clamp(high, low, x float64) float64 { switch { case high < x: