diff --git a/CHANGELOG.md b/CHANGELOG.md
index 816fa3ac..1e35e0c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,9 +42,10 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi
- Tab will insert a single indentation level when there are no suggestions
- Shift + Tab will delete a single indentation level when there are no suggestions and the line before the cursor consists only of indentation (spaces)
- Make `Completer` optional when creating a new `prompt.Prompt`. Change the signature of `prompt.New` from `func New(Executor, Completer, ...Option) *Prompt` to `func New(Executor, ...Option) *Prompt`
-- Make `prefix` optional in `prompt.Input`. Change the signature of `prompt.Input` from `func Input(string, ...Option) string` to `func Input(...Option) string`.
+- Make `prefix` and `completer` optional in `prompt.Input`. Change the signature of `prompt.Input` from `func Input(string, Completer, ...Option) string` to `func Input(...Option) string`.
- Rename `prompt.ConsoleParser` to `prompt.Reader` and make it embed `io.ReadCloser`
- Rename `prompt.ConsoleWriter` to `prompt.Writer` and make it embed `io.Writer` and `io.StringWriter`
+- Rename `prompt.Render` to `prompt.Renderer`
- Rename `prompt.OptionTitle` to `prompt.WithTitle`
- Rename `prompt.OptionPrefix` to `prompt.WithPrefix`
- Rename `prompt.OptionInitialBufferText` to `prompt.WithInitialText`
@@ -79,7 +80,7 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi
- Make pasting multiline text work properly
- Make pasting text with tabs work properly (tabs get replaced with indentation -- spaces)
-- Introduce `strings.ByteNumber`, `strings.RuneNumber`, `strings.StringWidth` to reduce the ambiguity of when to use which of the three main units used by this library to measure string length and index parts of strings. Several subtle bugs (using the wrong unit) causing panics have been fixed this way.
+- Introduce `strings.ByteNumber`, `strings.RuneNumber`, `strings.Width` to reduce the ambiguity of when to use which of the three main units used by this library to measure string length and index parts of strings. Several subtle bugs (using the wrong unit) causing panics have been fixed this way.
- Remove a `/dev/tty` leak in `prompt.PosixReader` (old `prompt.PosixParser`)
### Removed
diff --git a/README.md b/README.md
index 19ea9d0f..5c9a36dc 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,10 @@ func completer(d prompt.Document) []prompt.Suggest {
func main() {
fmt.Println("Please select table.")
- t := prompt.Input("> ", completer)
+ t := prompt.Input(
+ prompt.WithPrefix("> "),
+ prompt.WithCompleter(completer),
+ )
fmt.Println("You selected " + t)
}
```
diff --git a/_example/bang-executor/main.go b/_example/bang-executor/main.go
index 786ef551..b6bbb3c0 100644
--- a/_example/bang-executor/main.go
+++ b/_example/bang-executor/main.go
@@ -10,6 +10,7 @@ import (
func main() {
p := prompt.New(
executor,
+ prompt.WithPrefix(">>> "),
prompt.WithExecuteOnEnterCallback(ExecuteOnEnter),
)
@@ -18,7 +19,7 @@ func main() {
func ExecuteOnEnter(input string, indentSize int) (int, bool) {
char, _ := utf8.DecodeLastRuneInString(input)
- return 1, char == '!'
+ return 0, char == '!'
}
func executor(s string) {
diff --git a/buffer.go b/buffer.go
index a1d44e6e..0336bd45 100644
--- a/buffer.go
+++ b/buffer.go
@@ -37,7 +37,7 @@ func (b *Buffer) Document() (d *Document) {
// DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
// So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
-func (b *Buffer) DisplayCursorPosition(columns istrings.StringWidth) Position {
+func (b *Buffer) DisplayCursorPosition(columns istrings.Width) Position {
return b.Document().DisplayCursorPosition(columns)
}
diff --git a/completion.go b/completion.go
index 1ee6713c..1eabd56a 100644
--- a/completion.go
+++ b/completion.go
@@ -17,8 +17,8 @@ const (
)
var (
- leftMargin = runewidth.StringWidth(leftPrefix + leftSuffix)
- rightMargin = runewidth.StringWidth(rightPrefix + rightSuffix)
+ leftMargin = istrings.GetWidth(leftPrefix + leftSuffix)
+ rightMargin = istrings.GetWidth(rightPrefix + rightSuffix)
completionMargin = leftMargin + rightMargin
)
@@ -114,18 +114,18 @@ func deleteBreakLineCharacters(s string) string {
return s
}
-func formatTexts(o []string, max int, prefix, suffix string) (new []string, width int) {
+func formatTexts(o []string, max istrings.Width, prefix, suffix string) (new []string, width istrings.Width) {
l := len(o)
n := make([]string, l)
- lenPrefix := runewidth.StringWidth(prefix)
- lenSuffix := runewidth.StringWidth(suffix)
- lenShorten := runewidth.StringWidth(shortenSuffix)
+ lenPrefix := istrings.GetWidth(prefix)
+ lenSuffix := istrings.GetWidth(suffix)
+ lenShorten := istrings.GetWidth(shortenSuffix)
min := lenPrefix + lenSuffix + lenShorten
for i := 0; i < l; i++ {
o[i] = deleteBreakLineCharacters(o[i])
- w := runewidth.StringWidth(o[i])
+ w := istrings.GetWidth(o[i])
if width < w {
width = w
}
@@ -142,21 +142,21 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt
}
for i := 0; i < l; i++ {
- x := runewidth.StringWidth(o[i])
+ x := istrings.GetWidth(o[i])
if x <= width {
- spaces := strings.Repeat(" ", width-x)
+ spaces := strings.Repeat(" ", int(width-x))
n[i] = prefix + o[i] + spaces + suffix
} else if x > width {
- x := runewidth.Truncate(o[i], width, shortenSuffix)
+ x := runewidth.Truncate(o[i], int(width), shortenSuffix)
// When calling runewidth.Truncate("您好xxx您好xxx", 11, "...") returns "您好xxx..."
// But the length of this result is 10. So we need fill right using runewidth.FillRight.
- n[i] = prefix + runewidth.FillRight(x, width) + suffix
+ n[i] = prefix + runewidth.FillRight(x, int(width)) + suffix
}
}
return n, lenPrefix + width + lenSuffix
}
-func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width istrings.StringWidth) {
+func formatSuggestions(suggests []Suggest, max istrings.Width) (new []Suggest, width istrings.Width) {
num := len(suggests)
new = make([]Suggest, num)
@@ -178,7 +178,7 @@ func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width istrin
for i := 0; i < num; i++ {
new[i] = Suggest{Text: left[i], Description: right[i]}
}
- return new, istrings.StringWidth(leftWidth + rightWidth)
+ return new, istrings.Width(leftWidth + rightWidth)
}
// Constructor option for CompletionManager.
diff --git a/completion_test.go b/completion_test.go
index c2d13446..959d86f1 100644
--- a/completion_test.go
+++ b/completion_test.go
@@ -11,8 +11,8 @@ func TestFormatShortSuggestion(t *testing.T) {
var scenarioTable = []struct {
in []Suggest
expected []Suggest
- max int
- exWidth istrings.StringWidth
+ max istrings.Width
+ exWidth istrings.Width
}{
{
in: []Suggest{
@@ -40,7 +40,7 @@ func TestFormatShortSuggestion(t *testing.T) {
{Text: " coconut ", Description: " This is coconut. "},
},
max: 100,
- exWidth: istrings.StringWidth(len(" apple " + " This is apple. ")),
+ exWidth: istrings.Width(len(" apple " + " This is apple. ")),
},
{
in: []Suggest{
@@ -84,7 +84,7 @@ func TestFormatShortSuggestion(t *testing.T) {
{Text: " --include-extended-apis ", Description: " --------------... "},
},
max: 50,
- exWidth: istrings.StringWidth(len(" --include-extended-apis " + " ---------------...")),
+ exWidth: istrings.Width(len(" --include-extended-apis " + " ---------------...")),
},
{
in: []Suggest{
@@ -104,7 +104,7 @@ func TestFormatShortSuggestion(t *testing.T) {
{Text: " --include-extended-apis ", Description: " If true, include definitions of new APIs via calls to the API server. [default true] "},
},
max: 500,
- exWidth: istrings.StringWidth(len(" --include-extended-apis " + " If true, include definitions of new APIs via calls to the API server. [default true] ")),
+ exWidth: istrings.Width(len(" --include-extended-apis " + " If true, include definitions of new APIs via calls to the API server. [default true] ")),
},
}
@@ -123,8 +123,8 @@ func TestFormatText(t *testing.T) {
var scenarioTable = []struct {
in []string
expected []string
- max int
- exWidth int
+ max istrings.Width
+ exWidth istrings.Width
}{
{
in: []string{
@@ -163,7 +163,7 @@ func TestFormatText(t *testing.T) {
"",
"",
},
- max: len(" " + " " + shortenSuffix),
+ max: istrings.GetWidth(" " + " " + shortenSuffix),
exWidth: 0,
},
{
@@ -178,7 +178,7 @@ func TestFormatText(t *testing.T) {
" coconut ",
},
max: 100,
- exWidth: len(" coconut "),
+ exWidth: istrings.GetWidth(" coconut "),
},
{
in: []string{
diff --git a/constructor.go b/constructor.go
index ecb18a43..843f524c 100644
--- a/constructor.go
+++ b/constructor.go
@@ -13,7 +13,7 @@ const DefaultIndentSize = 2
// that constitute a single indentation level.
func WithIndentSize(i int) Option {
return func(p *Prompt) error {
- p.indentSize = i
+ p.renderer.indentSize = i
return nil
}
}
@@ -312,37 +312,14 @@ func DefaultPrefixCallback() string {
// New returns a Prompt with powerful auto-completion.
func New(executor Executor, opts ...Option) *Prompt {
- defaultWriter := NewStdoutWriter()
- registerWriter(defaultWriter)
-
pt := &Prompt{
- reader: NewStdinReader(),
- renderer: &Render{
- out: defaultWriter,
- prefixCallback: DefaultPrefixCallback,
- prefixTextColor: Blue,
- prefixBGColor: DefaultColor,
- inputTextColor: DefaultColor,
- inputBGColor: DefaultColor,
- previewSuggestionTextColor: Green,
- previewSuggestionBGColor: DefaultColor,
- suggestionTextColor: White,
- suggestionBGColor: Cyan,
- selectedSuggestionTextColor: Black,
- selectedSuggestionBGColor: Turquoise,
- descriptionTextColor: Black,
- descriptionBGColor: Turquoise,
- selectedDescriptionTextColor: White,
- selectedDescriptionBGColor: Cyan,
- scrollbarThumbColor: DarkGray,
- scrollbarBGColor: Cyan,
- },
+ reader: NewStdinReader(),
+ renderer: NewRenderer(),
buf: NewBuffer(),
executor: executor,
history: NewHistory(),
completion: NewCompletionManager(6),
executeOnEnterCallback: DefaultExecuteOnEnterCallback,
- indentSize: DefaultIndentSize,
keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting
}
diff --git a/document.go b/document.go
index 642c098c..94eddc02 100644
--- a/document.go
+++ b/document.go
@@ -35,7 +35,7 @@ func (d *Document) LastKeyStroke() Key {
// DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
// So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
-func (d *Document) DisplayCursorPosition(columns istrings.StringWidth) Position {
+func (d *Document) DisplayCursorPosition(columns istrings.Width) Position {
str := utf8string.NewString(d.Text).Slice(0, int(d.cursorPosition))
return positionAtEndOfString(str, columns)
}
@@ -381,12 +381,12 @@ func (d *Document) GetCursorRightPosition(count istrings.RuneNumber) istrings.Ru
}
// Get the current cursor position.
-func (d *Document) GetCursorPosition(columns istrings.StringWidth) Position {
+func (d *Document) GetCursorPosition(columns istrings.Width) Position {
return positionAtEndOfString(d.TextBeforeCursor(), columns)
}
// Get the position of the end of the current text.
-func (d *Document) GetEndOfTextPosition(columns istrings.StringWidth) Position {
+func (d *Document) GetEndOfTextPosition(columns istrings.Width) Position {
return positionAtEndOfString(d.Text, columns)
}
diff --git a/position.go b/position.go
index decccd40..14ef66f2 100644
--- a/position.go
+++ b/position.go
@@ -14,7 +14,7 @@ import (
// (0, 0) represents the top-left corner of the prompt,
// while (n, n) the bottom-right corner.
type Position struct {
- X istrings.StringWidth
+ X istrings.Width
Y int
}
@@ -47,16 +47,16 @@ func (p Position) Subtract(other Position) Position {
// positionAtEndOfString calculates the position of the
// p at the end of the given string.
-func positionAtEndOfString(str string, columns istrings.StringWidth) Position {
+func positionAtEndOfString(str string, columns istrings.Width) Position {
pos := positionAtEndOfReader(strings.NewReader(str), columns)
return pos
}
// positionAtEndOfReader calculates the position of the
// p at the end of the given io.Reader.
-func positionAtEndOfReader(reader io.RuneReader, columns istrings.StringWidth) Position {
+func positionAtEndOfReader(reader io.RuneReader, columns istrings.Width) Position {
var down int
- var right istrings.StringWidth
+ var right istrings.Width
charLoop:
for {
@@ -80,7 +80,7 @@ charLoop:
down++
right = 0
default:
- right += istrings.StringWidth(runewidth.RuneWidth(char))
+ right += istrings.Width(runewidth.RuneWidth(char))
if right == columns {
right = 0
down++
diff --git a/position_test.go b/position_test.go
index 49984d0b..cc9864ce 100644
--- a/position_test.go
+++ b/position_test.go
@@ -13,7 +13,7 @@ import (
func TestPositionAtEndOfString(t *testing.T) {
tests := map[string]struct {
input string
- columns istrings.StringWidth
+ columns istrings.Width
want Position
}{
"empty": {
diff --git a/prompt.go b/prompt.go
index 217b8fb4..e6ba2961 100644
--- a/prompt.go
+++ b/prompt.go
@@ -2,6 +2,8 @@ package prompt
import (
"bytes"
+ "fmt"
+ "log"
"os"
"strings"
"time"
@@ -41,7 +43,7 @@ type Completer func(Document) []Suggest
type Prompt struct {
reader Reader
buf *Buffer
- renderer *Render
+ renderer *Renderer
executor Executor
history *History
lexer Lexer
@@ -50,7 +52,6 @@ type Prompt struct {
ASCIICodeBindings []ASCIICodeBind
keyBindMode KeyBindMode
completionOnDown bool
- indentSize int // How many spaces constitute a single indentation level
exitChecker ExitChecker
executeOnEnterCallback ExecuteOnEnterCallback
skipClose bool
@@ -73,7 +74,7 @@ func (p *Prompt) Run() {
p.completion.Update(*p.buf.Document())
}
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
@@ -104,7 +105,7 @@ func (p *Prompt) Run() {
p.completion.Update(*p.buf.Document())
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
if p.exitChecker != nil && p.exitChecker(e.input, true) {
p.skipClose = true
@@ -116,11 +117,11 @@ func (p *Prompt) Run() {
go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
} else {
p.completion.Update(*p.buf.Document())
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
}
case w := <-winSizeCh:
p.renderer.UpdateWinSize(w)
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
case code := <-exitCh:
p.renderer.BreakLine(p.buf, p.lexer)
p.Close()
@@ -131,14 +132,14 @@ func (p *Prompt) Run() {
}
}
-// func Log(format string, a ...any) {
-// f, err := os.OpenFile("log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
-// if err != nil {
-// log.Fatalf("error opening file: %v", err)
-// }
-// defer f.Close()
-// fmt.Fprintf(f, format, a...)
-// }
+func Log(format string, a ...any) {
+ f, err := os.OpenFile("log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ log.Fatalf("error opening file: %v", err)
+ }
+ defer f.Close()
+ fmt.Fprintf(f, format, a...)
+}
func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) {
key := GetKey(b)
@@ -150,11 +151,11 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) {
keySwitch:
switch key {
case Enter, ControlJ, ControlM:
- indent, execute := p.executeOnEnterCallback(p.buf.Text(), p.indentSize)
+ indent, execute := p.executeOnEnterCallback(p.buf.Text(), p.renderer.indentSize)
if !execute {
p.buf.NewLine(false)
var indentStrBuilder strings.Builder
- indentUnitCount := indent * p.indentSize
+ indentUnitCount := indent * p.renderer.indentSize
for i := 0; i < indentUnitCount; i++ {
indentStrBuilder.WriteRune(IndentUnit)
}
@@ -180,7 +181,7 @@ keySwitch:
for _, byt := range b {
switch byt {
case '\t':
- for i := 0; i < p.indentSize; i++ {
+ for i := 0; i < p.renderer.indentSize; i++ {
newBytes = append(newBytes, IndentUnit)
}
default:
@@ -201,7 +202,7 @@ keySwitch:
break keySwitch
}
}
- p.buf.DeleteBeforeCursor(istrings.RuneNumber(p.indentSize))
+ p.buf.DeleteBeforeCursor(istrings.RuneNumber(p.renderer.indentSize))
case ControlC:
p.renderer.BreakLine(p.buf, p.lexer)
p.buf = NewBuffer()
@@ -336,7 +337,7 @@ func (p *Prompt) Input() string {
p.completion.Update(*p.buf.Document())
}
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
go p.readBuffer(bufCh, stopReadBufCh)
@@ -354,7 +355,7 @@ func (p *Prompt) Input() string {
return e.input
} else {
p.completion.Update(*p.buf.Document())
- p.renderer.Render(p.buf, p.completion, p.lexer)
+ p.renderer.Renderer(p.buf, p.completion, p.lexer)
}
default:
time.Sleep(10 * time.Millisecond)
@@ -395,7 +396,7 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) {
// translate \t into two spaces
// to avoid problems with cursor positions
case '\t':
- for i := 0; i < p.indentSize; i++ {
+ for i := 0; i < p.renderer.indentSize; i++ {
newBytes = append(newBytes, IndentUnit)
}
default:
diff --git a/render.go b/render.go
index 7cc69e05..14905c05 100644
--- a/render.go
+++ b/render.go
@@ -3,20 +3,23 @@ package prompt
import (
"runtime"
"strings"
+ "unicode/utf8"
"github.com/elk-language/go-prompt/debug"
istrings "github.com/elk-language/go-prompt/strings"
- runewidth "github.com/mattn/go-runewidth"
)
-// Render to render prompt information from state of Buffer.
-type Render struct {
+const multilinePrefixCharacter = '.'
+
+// Renderer to render prompt information from state of Buffer.
+type Renderer struct {
out Writer
prefixCallback PrefixCallback
breakLineCallback func(*Document)
title string
row uint16
- col istrings.StringWidth
+ col istrings.Width
+ indentSize int // How many spaces constitute a single indentation level
previousCursor Position
@@ -39,39 +42,61 @@ type Render struct {
scrollbarBGColor Color
}
+// Build a new Renderer.
+func NewRenderer() *Renderer {
+ defaultWriter := NewStdoutWriter()
+ registerWriter(defaultWriter)
+
+ return &Renderer{
+ out: defaultWriter,
+ indentSize: DefaultIndentSize,
+ prefixCallback: DefaultPrefixCallback,
+ prefixTextColor: Blue,
+ prefixBGColor: DefaultColor,
+ inputTextColor: DefaultColor,
+ inputBGColor: DefaultColor,
+ previewSuggestionTextColor: Green,
+ previewSuggestionBGColor: DefaultColor,
+ suggestionTextColor: White,
+ suggestionBGColor: Cyan,
+ selectedSuggestionTextColor: Black,
+ selectedSuggestionBGColor: Turquoise,
+ descriptionTextColor: Black,
+ descriptionBGColor: Turquoise,
+ selectedDescriptionTextColor: White,
+ selectedDescriptionBGColor: Cyan,
+ scrollbarThumbColor: DarkGray,
+ scrollbarBGColor: Cyan,
+ }
+}
+
// Setup to initialize console output.
-func (r *Render) Setup() {
+func (r *Renderer) Setup() {
if r.title != "" {
r.out.SetTitle(r.title)
debug.AssertNoError(r.out.Flush())
}
}
-// getCurrentPrefix to get current prefix.
-// If live-prefix is enabled, return live-prefix.
-func (r *Render) getCurrentPrefix() string {
- return r.prefixCallback()
-}
-
-func (r *Render) renderPrefix() {
+func (r *Renderer) renderPrefix(prefix string) {
r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false)
if _, err := r.out.WriteString("\r"); err != nil {
panic(err)
}
- if _, err := r.out.WriteString(r.getCurrentPrefix()); err != nil {
+ if _, err := r.out.WriteString(prefix); err != nil {
panic(err)
}
r.out.SetColor(DefaultColor, DefaultColor, false)
}
// Close to clear title and erasing.
-func (r *Render) Close() {
+func (r *Renderer) Close() {
r.out.ClearTitle()
r.out.EraseDown()
debug.AssertNoError(r.out.Flush())
}
-func (r *Render) prepareArea(lines int) {
+func (r *Renderer) prepareArea(lines int) {
for i := 0; i < lines; i++ {
r.out.ScrollDown()
}
@@ -81,12 +106,12 @@ func (r *Render) prepareArea(lines int) {
}
// UpdateWinSize called when window size is changed.
-func (r *Render) UpdateWinSize(ws *WinSize) {
+func (r *Renderer) UpdateWinSize(ws *WinSize) {
r.row = ws.Row
- r.col = istrings.StringWidth(ws.Col)
+ r.col = istrings.Width(ws.Col)
}
-func (r *Render) renderWindowTooSmall() {
+func (r *Renderer) renderWindowTooSmall() {
r.out.CursorGoTo(0, 0)
r.out.EraseScreen()
r.out.SetColor(DarkRed, White, false)
@@ -95,15 +120,15 @@ func (r *Render) renderWindowTooSmall() {
}
}
-func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
+func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) {
suggestions := completions.GetSuggestions()
if len(suggestions) == 0 {
return
}
- prefix := r.getCurrentPrefix()
+ prefix := r.prefixCallback()
formatted, width := formatSuggestions(
suggestions,
- int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
+ r.col-istrings.GetWidth(prefix)-1, // -1 means a width of scrollbar
)
// +1 means a width of scrollbar.
width++
@@ -181,8 +206,8 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
r.out.SetColor(DefaultColor, DefaultColor, false)
}
-// Render renders to the console.
-func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lexer) {
+// Renderer renders to the console.
+func (r *Renderer) Renderer(buffer *Buffer, completion *CompletionManager, lexer Lexer) {
// In situations where a pseudo 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 {
@@ -191,16 +216,17 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex
defer func() { debug.AssertNoError(r.out.Flush()) }()
r.clear(r.previousCursor)
- line := buffer.Text()
- prefix := r.getCurrentPrefix()
- prefixWidth := istrings.StringWidth(runewidth.StringWidth(prefix))
- cursor := positionAtEndOfString(prefix+line, r.col)
+ text := buffer.Text()
+ prefix := r.prefixCallback()
+ prefixWidth := istrings.GetWidth(prefix)
+ cursor := positionAtEndOfString(text, r.col)
+ cursor.X += prefixWidth
// prepare area
y := cursor.Y
h := y + 1 + int(completion.max)
- if h > int(r.row) || completionMargin > int(r.col) {
+ if h > int(r.row) || completionMargin > r.col {
r.renderWindowTooSmall()
return
}
@@ -209,48 +235,31 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex
r.out.HideCursor()
defer r.out.ShowCursor()
- r.renderPrefix()
-
- if lexer != nil {
- r.lex(lexer, line)
- } else {
- r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
- if _, err := r.out.WriteString(line); err != nil {
- panic(err)
- }
- }
+ r.renderText(lexer, text)
r.out.SetColor(DefaultColor, DefaultColor, false)
r.lineWrap(&cursor)
targetCursor := buffer.DisplayCursorPosition(r.col)
- if targetCursor.Y == 0 {
- targetCursor.X += prefixWidth
- }
+ targetCursor.X += prefixWidth
cursor = r.move(cursor, targetCursor)
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
- cursor = r.backward(cursor, istrings.StringWidth(runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator))))
+ cursor = r.backward(cursor, istrings.GetWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
if _, err := r.out.WriteString(suggest.Text); err != nil {
panic(err)
}
r.out.SetColor(DefaultColor, DefaultColor, false)
- cursor.X += istrings.StringWidth(runewidth.StringWidth(suggest.Text))
+ cursor.X += istrings.GetWidth(suggest.Text)
endOfSuggestionPos := cursor
rest := buffer.Document().TextAfterCursor()
- if lexer != nil {
- r.lex(lexer, rest)
- } else {
- if _, err := r.out.WriteString(rest); err != nil {
- panic(err)
- }
- }
+ r.renderText(lexer, text)
r.out.SetColor(DefaultColor, DefaultColor, false)
@@ -263,44 +272,121 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex
r.previousCursor = cursor
}
+func (r *Renderer) renderText(lexer Lexer, text string) {
+ if lexer != nil {
+ r.lex(lexer, text)
+ return
+ }
+
+ prefix := r.prefixCallback()
+ multilinePrefix := r.getMultilinePrefix(prefix)
+ firstIteration := true
+ var lineBuffer strings.Builder
+ for _, char := range text {
+ lineBuffer.WriteRune(char)
+ if char != '\n' {
+ continue
+ }
+
+ r.renderLine(prefix, lineBuffer.String(), r.inputTextColor)
+ lineBuffer.Reset()
+ if firstIteration {
+ prefix = multilinePrefix
+ firstIteration = false
+ }
+ }
+
+ r.renderLine(prefix, lineBuffer.String(), r.inputTextColor)
+}
+
+func (r *Renderer) renderLine(prefix, line string, color Color) {
+ r.renderPrefix(prefix)
+ r.writeString(line, color)
+}
+
+func (r *Renderer) writeString(text string, color Color) {
+ r.out.SetColor(color, r.inputBGColor, false)
+ if _, err := r.out.WriteString(text); err != nil {
+ panic(err)
+ }
+}
+
+func (r *Renderer) getMultilinePrefix(prefix string) string {
+ var spaceCount int
+ var dotCount int
+ var nonSpaceCharSeen bool
+ for {
+ if len(prefix) == 0 {
+ break
+ }
+ char, size := utf8.DecodeLastRuneInString(prefix)
+ prefix = prefix[:len(prefix)-size]
+ charWidth := istrings.GetRuneWidth(char)
+ if nonSpaceCharSeen {
+ dotCount += int(charWidth)
+ continue
+ }
+ if char != ' ' {
+ nonSpaceCharSeen = true
+ dotCount += int(charWidth)
+ continue
+ }
+ spaceCount += int(charWidth)
+ }
+
+ var multilinePrefixBuilder strings.Builder
+
+ for i := 0; i < dotCount; i++ {
+ multilinePrefixBuilder.WriteByte(multilinePrefixCharacter)
+ }
+ for i := 0; i < spaceCount; i++ {
+ multilinePrefixBuilder.WriteByte(IndentUnit)
+ }
+
+ return multilinePrefixBuilder.String()
+}
+
// lex processes the given input with the given lexer
// and writes the result
-func (r *Render) lex(lexer Lexer, input string) {
+func (r *Renderer) lex(lexer Lexer, input string) {
lexer.Init(input)
s := input
+ prefix := r.prefixCallback()
+ r.renderPrefix(prefix)
+ multilinePrefix := r.getMultilinePrefix(prefix)
for {
token, ok := lexer.Next()
if !ok {
break
}
- a := strings.SplitAfter(s, token.Lexeme())
- s = strings.TrimPrefix(s, a[0])
+ text := strings.SplitAfter(s, token.Lexeme())[0]
+ s = strings.TrimPrefix(s, text)
- r.out.SetColor(token.Color(), r.inputBGColor, false)
- if _, err := r.out.WriteString(a[0]); err != nil {
- panic(err)
+ var lineBuffer strings.Builder
+ for _, char := range text {
+ lineBuffer.WriteRune(char)
+ if char != '\n' {
+ continue
+ }
+
+ r.writeString(lineBuffer.String(), token.Color())
+ r.renderPrefix(multilinePrefix)
+ lineBuffer.Reset()
}
+ r.writeString(lineBuffer.String(), token.Color())
}
}
// BreakLine to break line.
-func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) {
- // Erasing and Render
- cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.getCurrentPrefix(), r.col)
+func (r *Renderer) BreakLine(buffer *Buffer, lexer Lexer) {
+ // Erasing and Renderer
+ cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.prefixCallback(), r.col)
r.clear(cursor)
- r.renderPrefix()
-
- if lexer != nil {
- r.lex(lexer, buffer.Document().Text+"\n")
- } else {
- r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
- if _, err := r.out.WriteString(buffer.Document().Text + "\n"); err != nil {
- panic(err)
- }
- }
+ text := buffer.Document().Text + "\n"
+ r.renderText(lexer, text)
r.out.SetColor(DefaultColor, DefaultColor, false)
@@ -314,27 +400,27 @@ func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) {
// clear erases the screen from a beginning of input
// even if there is line break which means input length exceeds a window's width.
-func (r *Render) clear(cursor Position) {
+func (r *Renderer) clear(cursor Position) {
r.move(cursor, Position{})
r.out.EraseDown()
}
// backward moves cursor to backward from a current cursor position
// regardless there is a line break.
-func (r *Render) backward(from Position, n istrings.StringWidth) Position {
+func (r *Renderer) backward(from Position, n istrings.Width) Position {
return r.move(from, Position{X: from.X - n, Y: from.Y})
}
// move moves cursor to specified position from the beginning of input
// even if there is a line break.
-func (r *Render) move(from, to Position) Position {
+func (r *Renderer) move(from, to Position) Position {
newPosition := from.Subtract(to)
r.out.CursorUp(newPosition.Y)
r.out.CursorBackward(int(newPosition.X))
return to
}
-func (r *Render) lineWrap(cursor *Position) {
+func (r *Renderer) lineWrap(cursor *Position) {
if runtime.GOOS != "windows" && cursor.X > 0 && cursor.X%r.col == 0 {
cursor.X = 0
cursor.Y += 1
@@ -353,7 +439,7 @@ func clamp(high, low, x float64) float64 {
}
}
-func alignNextLine(r *Render, col istrings.StringWidth) {
+func alignNextLine(r *Renderer, col istrings.Width) {
r.out.CursorDown(1)
if _, err := r.out.WriteString("\r"); err != nil {
panic(err)
diff --git a/render_test.go b/render_test.go
index d67a7ef2..7f35feea 100644
--- a/render_test.go
+++ b/render_test.go
@@ -18,8 +18,8 @@ func TestFormatCompletion(t *testing.T) {
prefix string
suffix string
expected []Suggest
- maxWidth int
- expectedWidth istrings.StringWidth
+ maxWidth istrings.Width
+ expectedWidth istrings.Width
}{
{
scenario: "",
@@ -74,29 +74,11 @@ func TestFormatCompletion(t *testing.T) {
func TestBreakLineCallback(t *testing.T) {
var i int
- r := &Render{
- out: &PosixWriter{
- fd: syscall.Stdin, // "write" to stdin just so we don't mess with the output of the tests
- },
- prefixCallback: DefaultPrefixCallback,
- prefixTextColor: Blue,
- prefixBGColor: DefaultColor,
- inputTextColor: DefaultColor,
- inputBGColor: DefaultColor,
- previewSuggestionTextColor: Green,
- previewSuggestionBGColor: DefaultColor,
- suggestionTextColor: White,
- suggestionBGColor: Cyan,
- selectedSuggestionTextColor: Black,
- selectedSuggestionBGColor: Turquoise,
- descriptionTextColor: Black,
- descriptionBGColor: Turquoise,
- selectedDescriptionTextColor: White,
- selectedDescriptionBGColor: Cyan,
- scrollbarThumbColor: DarkGray,
- scrollbarBGColor: Cyan,
- col: 1,
+ r := NewRenderer()
+ r.out = &PosixWriter{
+ fd: syscall.Stdin, // "write" to stdin just so we don't mess with the output of the tests
}
+ r.col = 1
b := NewBuffer()
r.BreakLine(b, nil)
@@ -115,3 +97,45 @@ func TestBreakLineCallback(t *testing.T) {
t.Errorf("BreakLine callback not called, i should be 3")
}
}
+
+func TestGetMultilinePrefix(t *testing.T) {
+ tests := map[string]struct {
+ prefix string
+ want string
+ }{
+ "single width chars": {
+ prefix: ">>",
+ want: "..",
+ },
+ "double width chars": {
+ prefix: "本日",
+ want: "....",
+ },
+ "trailing spaces and single width chars": {
+ prefix: ">!> ",
+ want: "... ",
+ },
+ "trailing spaces and double width chars": {
+ prefix: "本日: ",
+ want: "..... ",
+ },
+ "leading spaces and single width chars": {
+ prefix: " ah: ",
+ want: "..... ",
+ },
+ "leading spaces and double width chars": {
+ prefix: " 本日: ",
+ want: "....... ",
+ },
+ }
+
+ r := NewRenderer()
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ got := r.getMultilinePrefix(tc.prefix)
+ if tc.want != got {
+ t.Errorf("Expected %#v, but got %#v", tc.want, got)
+ }
+ })
+ }
+}
diff --git a/strings/strings.go b/strings/strings.go
index d89ec60f..8523faf8 100644
--- a/strings/strings.go
+++ b/strings/strings.go
@@ -2,6 +2,8 @@ package strings
import (
"unicode/utf8"
+
+ "github.com/mattn/go-runewidth"
)
// Get the length of the string in bytes.
@@ -14,6 +16,16 @@ func RuneCount(s string) RuneNumber {
return RuneNumber(utf8.RuneCountInString(s))
}
+// Get the width of the string (how many columns it takes upt in the terminal).
+func GetWidth(s string) Width {
+ return Width(runewidth.StringWidth(s))
+}
+
+// Get the width of the rune (how many columns it takes upt in the terminal).
+func GetRuneWidth(char rune) Width {
+ return Width(runewidth.RuneWidth(char))
+}
+
// IndexNotByte is similar with strings.IndexByte but showing the opposite behavior.
func IndexNotByte(s string, c byte) ByteNumber {
n := len(s)
diff --git a/strings/units.go b/strings/units.go
index 106b3432..2f74a0e9 100644
--- a/strings/units.go
+++ b/strings/units.go
@@ -10,4 +10,4 @@ type RuneNumber int
// Numeric type that represents the visible
// width of characters in a string as seen in a terminal emulator.
-type StringWidth int
+type Width int