From ccb34ce76d2e9513c84af82da46e1848d81f2a46 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Sun, 16 Jul 2023 10:26:13 +0200 Subject: [PATCH] Add multiline prefixes --- _example/bang-executor/main.go | 3 +- constructor.go | 4 +- prompt.go | 29 +++---- render.go | 142 ++++++++++++++++++++++++--------- 4 files changed, 122 insertions(+), 56 deletions(-) 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/constructor.go b/constructor.go index ecb18a43..e9fec4ee 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 } } @@ -319,6 +319,7 @@ func New(executor Executor, opts ...Option) *Prompt { reader: NewStdinReader(), renderer: &Render{ out: defaultWriter, + indentSize: DefaultIndentSize, prefixCallback: DefaultPrefixCallback, prefixTextColor: Blue, prefixBGColor: DefaultColor, @@ -342,7 +343,6 @@ func New(executor Executor, opts ...Option) *Prompt { 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/prompt.go b/prompt.go index 217b8fb4..2891e302 100644 --- a/prompt.go +++ b/prompt.go @@ -2,6 +2,8 @@ package prompt import ( "bytes" + "fmt" + "log" "os" "strings" "time" @@ -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 @@ -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() @@ -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 b79cccf5..599579ad 100644 --- a/render.go +++ b/render.go @@ -3,11 +3,14 @@ package prompt import ( "runtime" "strings" + "unicode/utf8" "github.com/elk-language/go-prompt/debug" istrings "github.com/elk-language/go-prompt/strings" ) +const multilinePrefixCharacter = '.' + // Render to render prompt information from state of Buffer. type Render struct { out Writer @@ -16,6 +19,7 @@ type Render struct { title string row uint16 col istrings.Width + indentSize int // How many spaces constitute a single indentation level previousCursor Position @@ -46,12 +50,12 @@ func (r *Render) Setup() { } } -func (r *Render) renderPrefix() { +func (r *Render) 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.prefixCallback()); err != nil { + if _, err := r.out.WriteString(prefix); err != nil { panic(err) } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -184,10 +188,11 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex defer func() { debug.AssertNoError(r.out.Flush()) }() r.clear(r.previousCursor) - line := buffer.Text() + text := buffer.Text() prefix := r.prefixCallback() prefixWidth := istrings.GetWidth(prefix) - cursor := positionAtEndOfString(prefix+line, r.col) + cursor := positionAtEndOfString(text, r.col) + cursor.X += prefixWidth // prepare area y := cursor.Y @@ -202,25 +207,14 @@ 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) @@ -237,13 +231,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex 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) @@ -256,25 +244,109 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.previousCursor = cursor } +func (r *Render) 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 *Render) renderLine(prefix, line string, color Color) { + r.renderPrefix(prefix) + r.writeString(line, color) +} + +func (r *Render) writeString(text string, color Color) { + r.out.SetColor(color, r.inputBGColor, false) + if _, err := r.out.WriteString(text); err != nil { + panic(err) + } +} + +func (r *Render) 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] + if nonSpaceCharSeen { + dotCount++ + continue + } + if char != ' ' { + nonSpaceCharSeen = true + dotCount++ + continue + } + spaceCount++ + } + + 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) { 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()) } } @@ -284,16 +356,8 @@ func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) { 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)