Skip to content

Commit

Permalink
Add multiline prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Verseth committed Jul 16, 2023
1 parent 2cde6ad commit ccb34ce
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 56 deletions.
3 changes: 2 additions & 1 deletion _example/bang-executor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func main() {
p := prompt.New(
executor,
prompt.WithPrefix(">>> "),
prompt.WithExecuteOnEnterCallback(ExecuteOnEnter),
)

Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
29 changes: 15 additions & 14 deletions prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package prompt

import (
"bytes"
"fmt"
"log"
"os"
"strings"
"time"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
142 changes: 103 additions & 39 deletions render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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())
}
}

Expand All @@ -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)

Expand Down

0 comments on commit ccb34ce

Please sign in to comment.