From 812a30a0f3e1f7dbe7757998d13e67211411b4b2 Mon Sep 17 00:00:00 2001 From: Stepan Rabotkin Date: Wed, 6 Oct 2021 17:03:31 +0300 Subject: [PATCH 1/3] feat: implement lexer support --- lexer.go | 34 +++++++++++++++++++++++++ option.go | 9 +++++++ prompt.go | 23 ++++++++--------- render.go | 67 ++++++++++++++++++++++++++++++++++++++++++++------ render_test.go | 9 ++++--- 5 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 lexer.go diff --git a/lexer.go b/lexer.go new file mode 100644 index 00000000..d6ee3539 --- /dev/null +++ b/lexer.go @@ -0,0 +1,34 @@ +package prompt + +// LexerFunc is a callback from render. +type LexerFunc = func(line string) []LexerElement + +// LexerElement is a element of lexer. +type LexerElement struct { + Color Color + Text string +} + +// Lexer is a struct with lexer param and function. +type Lexer struct { + IsEnabled bool + fn LexerFunc +} + +// NewLexer returns new Lexer. +func NewLexer() *Lexer { + return &Lexer{ + IsEnabled: false, + } +} + +// SetLexerFunction in lexer struct. +func (l *Lexer) SetLexerFunction(fn LexerFunc) { + l.IsEnabled = true + l.fn = fn +} + +// Process line with a custom function. +func (l *Lexer) Process(line string) []LexerElement { + return l.fn(line) +} diff --git a/option.go b/option.go index 9fbd6806..dbb877bf 100644 --- a/option.go +++ b/option.go @@ -266,6 +266,14 @@ func OptionSetExitCheckerOnInput(fn ExitChecker) Option { } } +// OptionSetLexer set lexer function and enable it. +func OptionSetLexer(fn LexerFunc) Option { + return func(p *Prompt) error { + p.lexer.SetLexerFunction(fn) + return nil + } +} + // New returns a Prompt with powerful auto-completion. func New(executor Executor, completer Completer, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() @@ -297,6 +305,7 @@ func New(executor Executor, completer Completer, opts ...Option) *Prompt { buf: NewBuffer(), executor: executor, history: NewHistory(), + lexer: NewLexer(), completion: NewCompletionManager(completer, 6), keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } diff --git a/prompt.go b/prompt.go index 173cd517..9fa70c3b 100644 --- a/prompt.go +++ b/prompt.go @@ -28,6 +28,7 @@ type Prompt struct { renderer *Render executor Executor history *History + lexer *Lexer completion *CompletionManager keyBindings []KeyBind ASCIICodeBindings []ASCIICodeBind @@ -54,7 +55,7 @@ func (p *Prompt) Run() { p.completion.Update(*p.buf.Document()) } - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) @@ -69,7 +70,7 @@ func (p *Prompt) Run() { select { case b := <-bufCh: if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) stopReadBufCh <- struct{}{} stopHandleSignalCh <- struct{}{} return @@ -85,7 +86,7 @@ func (p *Prompt) Run() { p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) if p.exitChecker != nil && p.exitChecker(e.input, true) { p.skipTearDown = true @@ -97,13 +98,13 @@ 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.renderer.Render(p.buf, p.completion, p.lexer) } case w := <-winSizeCh: p.renderer.UpdateWinSize(w) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) case code := <-exitCh: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) p.tearDown() os.Exit(code) default: @@ -121,7 +122,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { switch key { case Enter, ControlJ, ControlM: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) exec = &Exec{input: p.buf.Text()} p.buf = NewBuffer() @@ -129,7 +130,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { p.history.Add(exec.input) } case ControlC: - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) p.buf = NewBuffer() p.history.Clear() case Up, ControlP: @@ -240,7 +241,7 @@ func (p *Prompt) Input() string { p.completion.Update(*p.buf.Document()) } - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) go p.readBuffer(bufCh, stopReadBufCh) @@ -249,7 +250,7 @@ func (p *Prompt) Input() string { select { case b := <-bufCh: if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf) + p.renderer.BreakLine(p.buf, p.lexer) stopReadBufCh <- struct{}{} return "" } else if e != nil { @@ -258,7 +259,7 @@ func (p *Prompt) Input() string { return e.input } else { p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion) + p.renderer.Render(p.buf, p.completion, p.lexer) } default: time.Sleep(10 * time.Millisecond) diff --git a/render.go b/render.go index 299ac48b..bb9b681e 100644 --- a/render.go +++ b/render.go @@ -2,6 +2,7 @@ package prompt import ( "runtime" + "strings" "github.com/c-bata/go-prompt/internal/debug" runewidth "github.com/mattn/go-runewidth" @@ -167,7 +168,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { } // Render renders to the console. -func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { +func (r *Render) Render(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 { @@ -194,9 +195,25 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { defer r.out.ShowCursor() r.renderPrefix() - r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(line) + + if lexer.IsEnabled { + processed := lexer.Process(line) + var s = line + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.SetColor(r.inputTextColor, r.inputBGColor, false) + r.out.WriteStr(line) + } + r.out.SetColor(DefaultColor, DefaultColor, false) + r.lineWrap(cursor) r.out.EraseDown() @@ -213,7 +230,25 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { cursor += runewidth.StringWidth(suggest.Text) rest := buffer.Document().TextAfterCursor() - r.out.WriteStr(rest) + + if lexer.IsEnabled { + processed := lexer.Process(rest) + + var s = rest + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.WriteStr(rest) + } + + r.out.SetColor(DefaultColor, DefaultColor, false) + cursor += runewidth.StringWidth(rest) r.lineWrap(cursor) @@ -223,14 +258,32 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { } // BreakLine to break line. -func (r *Render) BreakLine(buffer *Buffer) { +func (r *Render) BreakLine(buffer *Buffer, lexer *Lexer) { // Erasing and Render cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix()) r.clear(cursor) + r.renderPrefix() - r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(buffer.Document().Text + "\n") + + if lexer.IsEnabled { + processed := lexer.Process(buffer.Document().Text + "\n") + + var s = buffer.Document().Text + "\n" + + for _, v := range processed { + a := strings.SplitAfter(s, v.Text) + s = strings.TrimPrefix(s, a[0]) + + r.out.SetColor(v.Color, r.inputBGColor, false) + r.out.WriteStr(a[0]) + } + } else { + r.out.SetColor(r.inputTextColor, r.inputBGColor, false) + r.out.WriteStr(buffer.Document().Text + "\n") + } + r.out.SetColor(DefaultColor, DefaultColor, false) + debug.AssertNoError(r.out.Flush()) if r.breakLineCallback != nil { r.breakLineCallback(buffer.Document()) diff --git a/render_test.go b/render_test.go index 0d9fa8a3..302f6e19 100644 --- a/render_test.go +++ b/render_test.go @@ -96,7 +96,8 @@ func TestBreakLineCallback(t *testing.T) { col: 1, } b := NewBuffer() - r.BreakLine(b) + l := NewLexer() + r.BreakLine(b, l) if i != 0 { t.Errorf("i should initially be 0, before applying a break line callback") @@ -105,9 +106,9 @@ func TestBreakLineCallback(t *testing.T) { r.breakLineCallback = func(doc *Document) { i++ } - r.BreakLine(b) - r.BreakLine(b) - r.BreakLine(b) + r.BreakLine(b, l) + r.BreakLine(b, l) + r.BreakLine(b, l) if i != 3 { t.Errorf("BreakLine callback not called, i should be 3") From dcf709eb3d7e44d3af012ef1fe739488494c116a Mon Sep 17 00:00:00 2001 From: Stepan Rabotkin Date: Wed, 6 Oct 2021 17:16:15 +0300 Subject: [PATCH 2/3] feat: add lexer example --- _example/build.sh | 1 + _example/even-lexer/main.go | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 _example/even-lexer/main.go diff --git a/_example/build.sh b/_example/build.sh index 4c552afc..7b71a4f5 100755 --- a/_example/build.sh +++ b/_example/build.sh @@ -10,3 +10,4 @@ go build -o ${BIN_DIR}/http-prompt ${DIR}/http-prompt/main.go go build -o ${BIN_DIR}/live-prefix ${DIR}/live-prefix/main.go go build -o ${BIN_DIR}/simple-echo ${DIR}/simple-echo/main.go go build -o ${BIN_DIR}/simple-echo-cjk-cyrillic ${DIR}/simple-echo/cjk-cyrillic/main.go +go build -o ${BIN_DIR}/even-lexer ${DIR}/even-lexer/main.go diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go new file mode 100644 index 00000000..f094f8ca --- /dev/null +++ b/_example/even-lexer/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "github.com/c-bata/go-prompt" + "strings" +) + +func main() { + p := prompt.New( + executor, + completer, + prompt.OptionSetLexer(lexer), + ) + + p.Run() +} + +func lexer(line string) []prompt.LexerElement { + var elements []prompt.LexerElement + + strArr := strings.Split(line, "") + + for k, v := range strArr { + element := prompt.LexerElement{ + Text: v, + } + + // every even char must be green. + if k % 2 == 0 { + element.Color = prompt.Green + } else { + element.Color = prompt.White + } + + elements = append(elements, element) + } + + return elements +} + +func completer(in prompt.Document) []prompt.Suggest { + return []prompt.Suggest{} +} + +func executor(s string) { + fmt.Println("You printed: " + s) +} From 410918d0f031a93c3c7c42cd3ea35c626cf3d0c6 Mon Sep 17 00:00:00 2001 From: Stepan Rabotkin Date: Wed, 6 Oct 2021 17:19:22 +0300 Subject: [PATCH 3/3] refac: go fmt files --- _example/even-lexer/main.go | 4 ++-- lexer.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go index f094f8ca..836ac57d 100644 --- a/_example/even-lexer/main.go +++ b/_example/even-lexer/main.go @@ -23,11 +23,11 @@ func lexer(line string) []prompt.LexerElement { for k, v := range strArr { element := prompt.LexerElement{ - Text: v, + Text: v, } // every even char must be green. - if k % 2 == 0 { + if k%2 == 0 { element.Color = prompt.Green } else { element.Color = prompt.White diff --git a/lexer.go b/lexer.go index d6ee3539..3f5cd76a 100644 --- a/lexer.go +++ b/lexer.go @@ -6,13 +6,13 @@ type LexerFunc = func(line string) []LexerElement // LexerElement is a element of lexer. type LexerElement struct { Color Color - Text string + Text string } // Lexer is a struct with lexer param and function. type Lexer struct { IsEnabled bool - fn LexerFunc + fn LexerFunc } // NewLexer returns new Lexer.