feat: linewrap support (#35)

* feat: linewrap support

* feat: support Terminal.app

* fix: fix corner case of renderCompletion

* refactor: remove verbose method

* refactor: inlining to stringWidth
This commit is contained in:
Nao YONASHIRO 2018-02-13 23:09:27 +09:00 committed by Masashi SHIBATA
parent a34adaee8d
commit 5f0f837e16

142
render.go
View File

@ -1,9 +1,5 @@
package prompt
import (
"math"
)
// Render to render prompt information from state of Buffer.
type Render struct {
out ConsoleWriter
@ -12,6 +8,9 @@ type Render struct {
title string
row uint16
col uint16
previousCursor int
// colors,
prefixTextColor Color
prefixBGColor Color
@ -88,48 +87,46 @@ func (r *Render) renderWindowTooSmall() {
}
func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
suggestions := completions.GetSuggestions()
if len(completions.GetSuggestions()) == 0 {
return
}
prefix := r.getCurrentPrefix()
formatted, width := formatSuggestions(
suggestions,
int(r.col)-len(prefix)-1, // -1 means a width of scrollbar
)
// +1 means a width of scrollbar.
width += 1
windowHeight := len(completions.tmp)
if windowHeight > int(completions.max) {
windowHeight = int(completions.max)
}
formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
r.prepareArea(windowHeight)
cursor := len(prefix) + len(buf.Document().TextBeforeCursor())
x, _ := r.toPos(cursor)
if x+width >= int(r.col) {
r.out.CursorBackward(x + width - int(r.col))
}
contentHeight := len(completions.tmp)
fractionVisible := float64(windowHeight) / float64(contentHeight)
fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
scrollbarHeight := int(math.Min(float64(windowHeight), math.Max(1, float64(windowHeight)*fractionVisible)))
scrollbarHeight := int(clamp(float64(windowHeight), 1, float64(windowHeight)*fractionVisible))
scrollbarTop := int(float64(windowHeight) * fractionAbove)
isScrollThumb := func(row int) bool {
return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
}
suggestions := completions.GetSuggestions()
if l := len(completions.GetSuggestions()); l == 0 {
return
}
prefix := r.getCurrentPrefix()
formatted, width := formatSuggestions(
suggestions,
int(r.col)-len(prefix)-1, // -1 means a width of scrollbar
)
formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
l := len(formatted)
r.prepareArea(windowHeight)
// +1 means a width of scrollbar.
d := (len(prefix) + len(buf.Document().TextBeforeCursor()) + 1) % int(r.col)
if d == 0 { // the cursor is on right end.
r.out.CursorBackward(width)
} else if d+width > int(r.col) {
r.out.CursorBackward(d + width - int(r.col))
}
selected := completions.selected - completions.verticalScroll
r.out.SetColor(White, Cyan, false)
for i := 0; i < l; i++ {
for i := 0; i < windowHeight; i++ {
r.out.CursorDown(1)
if i == selected {
r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
@ -151,17 +148,14 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
}
r.out.WriteStr(" ")
// +1 means a width of scrollbar.
r.out.CursorBackward(width + 1)
}
if d == 0 && len(prefix)+len(buf.Text()) != 0 { // the cursor is on right end.
// DON'T CURSOR DOWN HERE. Because the line doesn't erase properly.
r.out.CursorForward(width + 1)
} else if d+width > int(r.col) {
r.out.CursorForward(d + width - int(r.col))
r.out.CursorBackward(width)
}
r.out.CursorUp(l)
if x+width >= int(r.col) {
r.out.CursorForward(x + width - int(r.col))
}
r.out.CursorUp(windowHeight)
r.out.SetColor(DefaultColor, DefaultColor, false)
return
}
@ -170,16 +164,18 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
line := buffer.Text()
prefix := r.getCurrentPrefix()
cursor := len(prefix) + len(line)
// In situations where a psuedo tty is allocated (e.g. within a docker container),
// window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
if r.col > 0 {
// Erasing
r.out.CursorBackward(int(r.col) + len(line) + len(prefix))
r.out.EraseDown()
r.clear(r.previousCursor)
// prepare area
h := ((len(prefix) + len(line)) / int(r.col)) + 1 + int(completion.max)
_, y := r.toPos(cursor)
h := y + 1 + int(completion.max)
if h > int(r.row) || completionMargin > int(r.col) {
r.renderWindowTooSmall()
return
@ -188,31 +184,81 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
// Rendering
r.renderPrefix()
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
r.out.WriteStr(line)
r.out.SetColor(DefaultColor, DefaultColor, false)
r.out.CursorBackward(len([]rune(line)) - buffer.CursorPosition)
cursor = r.backward(cursor, len(line)-buffer.CursorPosition)
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
r.out.CursorBackward(len([]rune(buffer.Document().GetWordBeforeCursor())))
cursor = r.backward(cursor, len(buffer.Document().GetWordBeforeCursor()))
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
r.out.WriteStr(suggest.Text)
r.out.SetColor(DefaultColor, DefaultColor, false)
cursor += len(suggest.Text)
}
r.out.Flush()
r.previousCursor = cursor
}
// BreakLine to break line.
func (r *Render) BreakLine(buffer *Buffer) {
// CR
prefix := r.getCurrentPrefix()
r.out.CursorBackward(int(r.col) + len(buffer.Text()) + len(prefix))
// Erasing and Render
r.out.EraseDown()
cursor := len(buffer.Document().TextBeforeCursor()) + len(r.getCurrentPrefix())
r.clear(cursor)
r.renderPrefix()
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
r.out.WriteStr(buffer.Document().Text + "\n")
r.out.SetColor(DefaultColor, DefaultColor, false)
r.out.Flush()
r.previousCursor = 0
}
func (r *Render) clear(cursor int) {
r.backward(cursor, cursor)
r.out.EraseDown()
}
func (r *Render) backward(from, n int) int {
return r.move(from, from-n)
}
func (r *Render) move(from, to int) int {
_, fromY := r.toPos(from)
toX, toY := r.toPos(to)
r.out.CursorUp(fromY - toY)
r.out.WriteRaw([]byte{'\r'})
r.out.CursorForward(toX)
return to
}
// toPos returns the relative position from the beginning of the string.
// the coordinate system with the beginning of the string as (0,0) and the width as r.col.
// the cursor points to the next character, but it points to that character only at the right end (x == r.col - 1).
// x will not return 0 except for the first row.
func (r *Render) toPos(cursor int) (x, y int) {
col := int(r.col)
if cursor > 0 && cursor%col == 0 {
return col - 1, cursor/col - 1
}
return cursor % col, cursor / col
}
func clamp(high, low, x float64) float64 {
switch {
case high < x:
return high
case x < low:
return low
default:
return x
}
}