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:
parent
a34adaee8d
commit
5f0f837e16
142
render.go
142
render.go
@ -1,9 +1,5 @@
|
|||||||
package prompt
|
package prompt
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Render to render prompt information from state of Buffer.
|
// Render to render prompt information from state of Buffer.
|
||||||
type Render struct {
|
type Render struct {
|
||||||
out ConsoleWriter
|
out ConsoleWriter
|
||||||
@ -12,6 +8,9 @@ type Render struct {
|
|||||||
title string
|
title string
|
||||||
row uint16
|
row uint16
|
||||||
col uint16
|
col uint16
|
||||||
|
|
||||||
|
previousCursor int
|
||||||
|
|
||||||
// colors,
|
// colors,
|
||||||
prefixTextColor Color
|
prefixTextColor Color
|
||||||
prefixBGColor Color
|
prefixBGColor Color
|
||||||
@ -88,48 +87,46 @@ func (r *Render) renderWindowTooSmall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
|
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)
|
windowHeight := len(completions.tmp)
|
||||||
if windowHeight > int(completions.max) {
|
if windowHeight > int(completions.max) {
|
||||||
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)
|
contentHeight := len(completions.tmp)
|
||||||
|
|
||||||
fractionVisible := float64(windowHeight) / float64(contentHeight)
|
fractionVisible := float64(windowHeight) / float64(contentHeight)
|
||||||
fractionAbove := float64(completions.verticalScroll) / 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)
|
scrollbarTop := int(float64(windowHeight) * fractionAbove)
|
||||||
|
|
||||||
isScrollThumb := func(row int) bool {
|
isScrollThumb := func(row int) bool {
|
||||||
return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
|
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
|
selected := completions.selected - completions.verticalScroll
|
||||||
|
|
||||||
r.out.SetColor(White, Cyan, false)
|
r.out.SetColor(White, Cyan, false)
|
||||||
for i := 0; i < l; i++ {
|
for i := 0; i < windowHeight; i++ {
|
||||||
r.out.CursorDown(1)
|
r.out.CursorDown(1)
|
||||||
if i == selected {
|
if i == selected {
|
||||||
r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
|
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.SetColor(DefaultColor, r.scrollbarBGColor, false)
|
||||||
}
|
}
|
||||||
r.out.WriteStr(" ")
|
r.out.WriteStr(" ")
|
||||||
// +1 means a width of scrollbar.
|
r.out.CursorBackward(width)
|
||||||
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.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)
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -170,16 +164,18 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
|
|||||||
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
|
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
|
||||||
line := buffer.Text()
|
line := buffer.Text()
|
||||||
prefix := r.getCurrentPrefix()
|
prefix := r.getCurrentPrefix()
|
||||||
|
cursor := len(prefix) + len(line)
|
||||||
|
|
||||||
// In situations where a psuedo tty is allocated (e.g. within a docker container),
|
// 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.
|
// window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
|
||||||
if r.col > 0 {
|
if r.col > 0 {
|
||||||
// Erasing
|
// Erasing
|
||||||
r.out.CursorBackward(int(r.col) + len(line) + len(prefix))
|
r.clear(r.previousCursor)
|
||||||
r.out.EraseDown()
|
|
||||||
|
|
||||||
// prepare area
|
// 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) {
|
if h > int(r.row) || completionMargin > int(r.col) {
|
||||||
r.renderWindowTooSmall()
|
r.renderWindowTooSmall()
|
||||||
return
|
return
|
||||||
@ -188,31 +184,81 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
|
|||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
r.renderPrefix()
|
r.renderPrefix()
|
||||||
|
|
||||||
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
||||||
r.out.WriteStr(line)
|
r.out.WriteStr(line)
|
||||||
r.out.SetColor(DefaultColor, DefaultColor, false)
|
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)
|
r.renderCompletion(buffer, completion)
|
||||||
if suggest, ok := completion.GetSelectedSuggestion(); ok {
|
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.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
|
||||||
r.out.WriteStr(suggest.Text)
|
r.out.WriteStr(suggest.Text)
|
||||||
r.out.SetColor(DefaultColor, DefaultColor, false)
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
||||||
|
|
||||||
|
cursor += len(suggest.Text)
|
||||||
}
|
}
|
||||||
r.out.Flush()
|
r.out.Flush()
|
||||||
|
|
||||||
|
r.previousCursor = cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreakLine to break line.
|
// BreakLine to break line.
|
||||||
func (r *Render) BreakLine(buffer *Buffer) {
|
func (r *Render) BreakLine(buffer *Buffer) {
|
||||||
// CR
|
|
||||||
prefix := r.getCurrentPrefix()
|
|
||||||
r.out.CursorBackward(int(r.col) + len(buffer.Text()) + len(prefix))
|
|
||||||
// Erasing and Render
|
// Erasing and Render
|
||||||
r.out.EraseDown()
|
cursor := len(buffer.Document().TextBeforeCursor()) + len(r.getCurrentPrefix())
|
||||||
|
r.clear(cursor)
|
||||||
r.renderPrefix()
|
r.renderPrefix()
|
||||||
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
|
||||||
r.out.WriteStr(buffer.Document().Text + "\n")
|
r.out.WriteStr(buffer.Document().Text + "\n")
|
||||||
r.out.SetColor(DefaultColor, DefaultColor, false)
|
r.out.SetColor(DefaultColor, DefaultColor, false)
|
||||||
r.out.Flush()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user