From ace1970445876a25fb9e6bda0a22b1cdb7a72d8a Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Tue, 25 Jul 2023 20:45:31 +0200 Subject: [PATCH] Add more formatting options for tokens --- CHANGELOG.md | 30 +++++++++++++++++- _example/even-lexer/main.go | 19 +++++++++--- buffer.go | 61 ++++++++++++++++++++++--------------- lexer.go | 55 ++++++++++++++++++++++++++++----- lexer_test.go | 2 +- prompt.go | 4 +-- renderer.go | 24 +++++++++++---- writer.go | 3 ++ writer_vt100.go | 2 +- 9 files changed, 153 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8672e82d..52d2b07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 25.07.2023 -## [1.0.0] +[Diff](https://github.com/elk-language/go-prompt/compare/v1.0.1...elk-language:go-prompt:v1.0.2) + +### Added + +- `prompt.Token` has new methods: + - `BackgroundColor() prompt.Color` - define the background color for the token + - `DisplayAttributes() []prompt.DisplayAttribute` - define the font eg. bold, italic, underline +- `prompt.NewSimpleToken` has new options: + - `prompt.SimpleTokenWithColor(c Color) SimpleTokenOption` + - `prompt.SimpleTokenWithBackgroundColor(c Color) SimpleTokenOption` + - `prompt.SimpleTokenWithDisplayAttributes(attrs ...DisplayAttribute) SimpleTokenOption` +- `prompt.Writer` has new methods: + - `prompt.SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribute)` + +### Changed + +- change the signature of `prompt.NewSimpleToken` from `func NewSimpleToken(color Color, firstIndex, lastIndex istrings.ByteNumber) *SimpleToken` to `func NewSimpleToken(firstIndex, lastIndex istrings.ByteNumber, opts ...SimpleTokenOption) *SimpleToken` + +## [1.0.1] - 25.07.2023 + +[Diff](https://github.com/elk-language/go-prompt/compare/v1.0.0...elk-language:go-prompt:v1.0.1) + +### Added + +- `prompt.Token` has a new method `FirstByteIndex() strings.ByteNumber` + + +## [1.0.0] - 25.07.2023 [Diff](https://github.com/elk-language/go-prompt/compare/v0.2.6...elk-language:go-prompt:v1.0.0) diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go index bb6a3f1d..dbbbbe85 100644 --- a/_example/even-lexer/main.go +++ b/_example/even-lexer/main.go @@ -32,7 +32,11 @@ func charLexer(line string) []prompt.Token { color = prompt.White } lastByteIndex := strings.ByteNumber(i + utf8.RuneLen(value) - 1) - element := prompt.NewSimpleToken(color, strings.ByteNumber(i), lastByteIndex) + element := prompt.NewSimpleToken( + strings.ByteNumber(i), + lastByteIndex, + prompt.SimpleTokenWithColor(color), + ) elements = append(elements, element) } @@ -67,7 +71,11 @@ func wordLexer(line string) []prompt.Token { color = prompt.White } - element := prompt.NewSimpleToken(color, firstByte, currentByte-1) + element := prompt.NewSimpleToken( + firstByte, + currentByte-1, + prompt.SimpleTokenWithColor(color), + ) elements = append(elements, element) wordIndex++ firstCharSeen = false @@ -84,11 +92,14 @@ func wordLexer(line string) []prompt.Token { } else { color = prompt.White } - element := prompt.NewSimpleToken(color, firstByte, currentByte+strings.ByteNumber(utf8.RuneLen(lastChar))-1) + element := prompt.NewSimpleToken( + firstByte, + currentByte+strings.ByteNumber(utf8.RuneLen(lastChar))-1, + prompt.SimpleTokenWithColor(color), + ) elements = append(elements, element) } - prompt.Log("tokens: %#v", elements) return elements } diff --git a/buffer.go b/buffer.go index a54c7d11..6232f132 100644 --- a/buffer.go +++ b/buffer.go @@ -9,12 +9,13 @@ import ( // Buffer emulates the console buffer. type Buffer struct { - workingLines []string // The working lines. Similar to history - workingIndex int // index of the current line - startLine int // Line number of the first visible line in the terminal (0-indexed) - cursorPosition istrings.RuneNumber - cacheDocument *Document - lastKeyStroke Key + workingLines []string // The working lines. Similar to history + workingIndex int // index of the current line + startLine int // Line number of the first visible line in the terminal (0-indexed) + cursorPosition istrings.RuneNumber + cacheDocument *Document + preferredColumn istrings.RuneNumber // Remember the original column for the next up/down movement. + lastKeyStroke Key } // Text returns string of the current line. @@ -22,6 +23,14 @@ func (b *Buffer) Text() string { return b.workingLines[b.workingIndex] } +func (b *Buffer) resetPreferredColumn() { + b.preferredColumn = -1 +} + +func (b *Buffer) updatePreferredColumn() { + b.preferredColumn = b.Document().CursorPositionCol() +} + // Document method to return document instance from the current text and cursor position. func (b *Buffer) Document() (d *Document) { if b.cacheDocument == nil || @@ -80,16 +89,17 @@ func (b *Buffer) insertText(text string, columns istrings.Width, rows int, overw if moveCursor { b.cursorPosition += istrings.RuneCount(text) - b.RecalculateStartLine(columns, rows) + b.recalculateStartLine(columns, rows) + b.updatePreferredColumn() } } -func (b *Buffer) ResetStartLine() { +func (b *Buffer) resetStartLine() { b.startLine = 0 } // Calculates the startLine once again and returns true when it's been changed. -func (b *Buffer) RecalculateStartLine(columns istrings.Width, rows int) bool { +func (b *Buffer) recalculateStartLine(columns istrings.Width, rows int) bool { origStartLine := b.startLine pos := b.DisplayCursorPosition(columns) if pos.Y > b.startLine+rows-1 { @@ -110,7 +120,8 @@ func (b *Buffer) RecalculateStartLine(columns istrings.Width, rows int) bool { func (b *Buffer) setText(text string, col istrings.Width, row int) { debug.Assert(b.cursorPosition <= istrings.RuneCount(text), "length of input should be shorter than cursor position") b.workingLines[b.workingIndex] = text - b.RecalculateStartLine(col, row) + b.recalculateStartLine(col, row) + b.resetPreferredColumn() } // Set cursor position. Return whether it changed. @@ -126,7 +137,8 @@ func (b *Buffer) setDocument(d *Document, columns istrings.Width, rows int) { b.cacheDocument = d b.setCursorPosition(d.cursorPosition) // Call before setText because setText check the relation between cursorPosition and line length. b.setText(d.Text, columns, rows) - b.RecalculateStartLine(columns, rows) + b.recalculateStartLine(columns, rows) + b.resetPreferredColumn() } // Move to the left on the current line. @@ -134,7 +146,8 @@ func (b *Buffer) setDocument(d *Document, columns istrings.Width, rows int) { func (b *Buffer) CursorLeft(count istrings.RuneNumber, columns istrings.Width, rows int) bool { l := b.Document().GetCursorLeftPosition(count) b.cursorPosition += l - return b.RecalculateStartLine(columns, rows) + b.updatePreferredColumn() + return b.recalculateStartLine(columns, rows) } // Move to the right on the current line. @@ -142,25 +155,24 @@ func (b *Buffer) CursorLeft(count istrings.RuneNumber, columns istrings.Width, r func (b *Buffer) CursorRight(count istrings.RuneNumber, columns istrings.Width, rows int) bool { l := b.Document().GetCursorRightPosition(count) b.cursorPosition += l - return b.RecalculateStartLine(columns, rows) + b.updatePreferredColumn() + return b.recalculateStartLine(columns, rows) } // CursorUp move cursor to the previous line. // (for multi-line edit). // Returns true when the view should be rerendered. func (b *Buffer) CursorUp(count int, columns istrings.Width, rows int) bool { - orig := b.Document().CursorPositionCol() - b.cursorPosition += b.Document().GetCursorUpPosition(count, orig) - return b.RecalculateStartLine(columns, rows) + b.cursorPosition += b.Document().GetCursorUpPosition(count, b.preferredColumn) + return b.recalculateStartLine(columns, rows) } // CursorDown move cursor to the next line. // (for multi-line edit). // Returns true when the view should be rerendered. func (b *Buffer) CursorDown(count int, columns istrings.Width, rows int) bool { - orig := b.Document().CursorPositionCol() - b.cursorPosition += b.Document().GetCursorDownPosition(count, orig) - return b.RecalculateStartLine(columns, rows) + b.cursorPosition += b.Document().GetCursorDownPosition(count, b.preferredColumn) + return b.recalculateStartLine(columns, rows) } // DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text. @@ -179,7 +191,8 @@ func (b *Buffer) DeleteBeforeCursor(count istrings.RuneNumber, columns istrings. cursorPosition: b.cursorPosition - istrings.RuneNumber(len([]rune(deleted))), }, columns, rows) } - b.RecalculateStartLine(columns, rows) + b.recalculateStartLine(columns, rows) + b.updatePreferredColumn() return } @@ -206,7 +219,6 @@ func (b *Buffer) Delete(count istrings.RuneNumber, col istrings.Width, row int) ) deleted := string(deletedRunes) - b.RecalculateStartLine(col, row) return deleted } @@ -243,9 +255,10 @@ func (b *Buffer) SwapCharactersBeforeCursor(col istrings.Width, row int) { // NewBuffer is constructor of Buffer struct. func NewBuffer() (b *Buffer) { b = &Buffer{ - workingLines: []string{""}, - workingIndex: 0, - startLine: 0, + workingLines: []string{""}, + workingIndex: 0, + startLine: 0, + preferredColumn: -1, } return } diff --git a/lexer.go b/lexer.go index b84c6832..89e338a3 100644 --- a/lexer.go +++ b/lexer.go @@ -15,32 +15,71 @@ type Lexer interface { // Token is a single unit of text returned by a Lexer. type Token interface { - Color() Color // Color of the token + Color() Color // Color of the token's text + BackgroundColor() Color + DisplayAttributes() []DisplayAttribute FirstByteIndex() istrings.ByteNumber // Index of the last byte of this token LastByteIndex() istrings.ByteNumber // Index of the last byte of this token } // SimpleToken as the default implementation of Token. type SimpleToken struct { - color Color - lastByteIndex istrings.ByteNumber - firstByteIndex istrings.ByteNumber + color Color + backgroundColor Color + displayAttributes []DisplayAttribute + lastByteIndex istrings.ByteNumber + firstByteIndex istrings.ByteNumber +} + +type SimpleTokenOption func(*SimpleToken) + +func SimpleTokenWithColor(c Color) SimpleTokenOption { + return func(t *SimpleToken) { + t.color = c + } +} + +func SimpleTokenWithBackgroundColor(c Color) SimpleTokenOption { + return func(t *SimpleToken) { + t.backgroundColor = c + } +} + +func SimpleTokenWithDisplayAttributes(attrs ...DisplayAttribute) SimpleTokenOption { + return func(t *SimpleToken) { + t.displayAttributes = attrs + } } // Create a new SimpleToken. -func NewSimpleToken(color Color, firstIndex, lastIndex istrings.ByteNumber) *SimpleToken { - return &SimpleToken{ - color: color, +func NewSimpleToken(firstIndex, lastIndex istrings.ByteNumber, opts ...SimpleTokenOption) *SimpleToken { + t := &SimpleToken{ firstByteIndex: firstIndex, lastByteIndex: lastIndex, } + + for _, opt := range opts { + opt(t) + } + + return t } -// Retrieve the color of this token. +// Retrieve the text color of this token. func (t *SimpleToken) Color() Color { return t.color } +// Retrieve the background color of this token. +func (t *SimpleToken) BackgroundColor() Color { + return t.backgroundColor +} + +// Retrieve the display attributes of this token eg. bold, underline. +func (t *SimpleToken) DisplayAttributes() []DisplayAttribute { + return t.displayAttributes +} + // The index of the last byte of the lexeme. func (t *SimpleToken) LastByteIndex() istrings.ByteNumber { return t.lastByteIndex diff --git a/lexer_test.go b/lexer_test.go index 71b34b65..17e84081 100644 --- a/lexer_test.go +++ b/lexer_test.go @@ -70,7 +70,7 @@ func TestEagerLexerNext(t *testing.T) { func charLex(s string) []Token { var result []Token for i := range s { - result = append(result, NewSimpleToken(0, istrings.ByteNumber(i), istrings.ByteNumber(i))) + result = append(result, NewSimpleToken(istrings.ByteNumber(i), istrings.ByteNumber(i))) } return result diff --git a/prompt.go b/prompt.go index 32cc2a00..bebc1cfa 100644 --- a/prompt.go +++ b/prompt.go @@ -125,8 +125,8 @@ func (p *Prompt) Run() { } case w := <-winSizeCh: p.renderer.UpdateWinSize(w) - p.Buffer.ResetStartLine() - p.Buffer.RecalculateStartLine(p.renderer.UserInputColumns(), int(p.renderer.row)) + p.Buffer.resetStartLine() + p.Buffer.recalculateStartLine(p.renderer.UserInputColumns(), int(p.renderer.row)) p.renderer.Render(p.Buffer, p.completion, p.lexer) case code := <-exitCh: p.renderer.BreakLine(p.Buffer, p.lexer) diff --git a/renderer.go b/renderer.go index 22009568..ff04573e 100644 --- a/renderer.go +++ b/renderer.go @@ -297,8 +297,7 @@ func (r *Renderer) writeStringColor(text string, color Color) { } } -func (r *Renderer) writeColor(b []byte, color Color) { - r.out.SetColor(color, r.inputBGColor, false) +func (r *Renderer) write(b []byte) { if _, err := r.out.Write(b); err != nil { panic(err) } @@ -366,20 +365,28 @@ tokenLoop: var currentFirstByteIndex istrings.ByteNumber var currentLastByteIndex istrings.ByteNumber var tokenColor Color + var tokenBackgroundColor Color + var tokenDisplayAttributes []DisplayAttribute var noToken bool if ok { currentFirstByteIndex = token.FirstByteIndex() currentLastByteIndex = token.LastByteIndex() tokenColor = token.Color() + tokenBackgroundColor = token.BackgroundColor() + tokenDisplayAttributes = token.DisplayAttributes() } else if previousByteIndex == istrings.Len(input)-1 { break tokenLoop } else { currentFirstByteIndex = istrings.Len(input) - tokenColor = DefaultColor + tokenColor = r.inputTextColor + tokenBackgroundColor = r.inputBGColor + tokenDisplayAttributes = nil noToken = true } - color := DefaultColor + color := r.inputTextColor + backgroundColor := r.inputBGColor + displayAttributes := tokenDisplayAttributes text := input[previousByteIndex+1 : currentFirstByteIndex] previousByteIndex = currentLastByteIndex lineBuffer = lineBuffer[:0] @@ -400,7 +407,8 @@ tokenLoop: break tokenLoop } lineBuffer = append(lineBuffer, '\n') - r.writeColor(lineBuffer, color) + r.out.SetDisplayAttributes(color, backgroundColor, displayAttributes...) + r.write(lineBuffer) r.renderPrefix(multilinePrefix) lineBuffer = lineBuffer[:0] if char != '\n' { @@ -419,7 +427,8 @@ tokenLoop: lineBuffer = append(lineBuffer, runeBuffer[:size]...) } if len(lineBuffer) > 0 { - r.writeColor(lineBuffer, color) + r.out.SetDisplayAttributes(color, backgroundColor, displayAttributes...) + r.write(lineBuffer) } if !interToken { @@ -430,12 +439,15 @@ tokenLoop: break tokenLoop } color = tokenColor + backgroundColor = tokenBackgroundColor + displayAttributes = tokenDisplayAttributes text = input[currentFirstByteIndex : currentLastByteIndex+1] lineBuffer = lineBuffer[:0] interToken = false } } + r.out.SetDisplayAttributes(r.inputTextColor, r.inputBGColor, DisplayReset) } // BreakLine to break line. diff --git a/writer.go b/writer.go index 7d1df825..398ee575 100644 --- a/writer.go +++ b/writer.go @@ -159,4 +159,7 @@ type Writer interface { // SetColor sets text and background colors. and specify whether text is bold. SetColor(fg, bg Color, bold bool) + + // Sets the colors and display attributes. + SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribute) } diff --git a/writer_vt100.go b/writer_vt100.go index f884cd64..a6e5fe5a 100644 --- a/writer_vt100.go +++ b/writer_vt100.go @@ -219,7 +219,7 @@ func (w *VT100Writer) SetColor(fg, bg Color, bold bool) { w.SetDisplayAttributes(fg, bg, DisplayBold) } else { // If using `DisplayDefualt`, it will be broken in some environment. - // Details are https://github.com/elk-language/go-prompt/pull/85 + // Details are https://github.com/c-bata/go-prompt/pull/85 w.SetDisplayAttributes(fg, bg, DisplayReset) } }