Skip to content

Commit

Permalink
Merge pull request #14 from elk-language/scrolling-buffer
Browse files Browse the repository at this point in the history
Render using a scrolling buffer
  • Loading branch information
Verseth authored Jul 23, 2023
2 parents cc24fc0 + 5c12400 commit ee09fb5
Show file tree
Hide file tree
Showing 26 changed files with 793 additions and 383 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi
- Rename `prompt.OptionPrefixBackgroundColor` to `prompt.WithPrefixBackgroundColor`
- Rename `prompt.OptionInputTextColor` to `prompt.WithInputTextColor`
- Rename `prompt.OptionInputBGColor` to `prompt.WithInputBGColor`
- Rename `prompt.OptionPreviewSuggestionTextColor` to `prompt.WithPreviewSuggestionTextColor`
- Rename `prompt.OptionSuggestionTextColor` to `prompt.WithSuggestionTextColor`
- Rename `prompt.OptionSuggestionBGColor` to `prompt.WithSuggestionBGColor`
- Rename `prompt.OptionSelectedSuggestionTextColor` to `prompt.WithSelectedSuggestionTextColor`
Expand All @@ -75,6 +74,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi
- Rename `prompt.OptionShowCompletionAtStart` to `prompt.WithShowCompletionAtStart`
- Rename `prompt.OptionBreakLineCallback` to `prompt.WithBreakLineCallback`
- Rename `prompt.OptionExitChecker` to `prompt.WithExitChecker`
- Change the signature of `Completer` from `func(Document) []Suggest` to `func(Document) (suggestions []Suggest, startChar, endChar istrings.RuneNumber)`
- Change the signature of `KeyBindFunc` from `func(*Buffer)` to `func(p *Prompt) (rerender bool)`

### Fixed

Expand All @@ -86,7 +87,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi
### Removed

- `prompt.SwitchKeyBindMode`

- `prompt.OptionPreviewSuggestionTextColor`
- `prompt.OptionPreviewSuggestionBGColor`

## [0.2.6] - 2021-03-03

Expand Down
10 changes: 5 additions & 5 deletions _example/even-lexer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package main

import (
"fmt"
"strings"
"unicode/utf8"

"github.com/elk-language/go-prompt"
"github.com/elk-language/go-prompt/strings"
)

func main() {
Expand All @@ -19,17 +20,16 @@ func main() {
func lexer(line string) []prompt.Token {
var elements []prompt.Token

strArr := strings.Split(line, "")

for i, value := range strArr {
for i, value := range line {
var color prompt.Color
// every even char must be green.
if i%2 == 0 {
color = prompt.Green
} else {
color = prompt.White
}
element := prompt.NewSimpleToken(color, value)
lastByteIndex := strings.ByteNumber(i + utf8.RuneLen(value) - 1)
element := prompt.NewSimpleToken(color, lastByteIndex)

elements = append(elements, element)
}
Expand Down
9 changes: 2 additions & 7 deletions _example/exec-command/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"os"
"os/exec"

Expand All @@ -9,6 +10,7 @@ import (

func executor(t string) {
if t != "bash" {
fmt.Println("Sorry, I don't understand.")
return
}

Expand All @@ -19,16 +21,9 @@ func executor(t string) {
cmd.Run()
}

func completer(t prompt.Document) []prompt.Suggest {
return []prompt.Suggest{
{Text: "bash"},
}
}

func main() {
p := prompt.New(
executor,
prompt.WithCompleter(completer),
)
p.Run()
}
9 changes: 6 additions & 3 deletions _example/http-prompt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

prompt "github.com/elk-language/go-prompt"
istrings "github.com/elk-language/go-prompt/strings"
)

type RequestContext struct {
Expand Down Expand Up @@ -157,12 +158,14 @@ func executor(in string) {
}
}

func completer(in prompt.Document) []prompt.Suggest {
func completer(in prompt.Document) ([]prompt.Suggest, istrings.RuneNumber, istrings.RuneNumber) {
endIndex := in.CurrentRuneIndex()
w := in.GetWordBeforeCursor()
if w == "" {
return []prompt.Suggest{}
return []prompt.Suggest{}, 0, 0
}
return prompt.FilterHasPrefix(suggestions, w, true)
startIndex := endIndex - istrings.RuneCount(w)
return prompt.FilterHasPrefix(suggestions, w, true), startIndex, endIndex
}

func main() {
Expand Down
9 changes: 7 additions & 2 deletions _example/live-prefix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

prompt "github.com/elk-language/go-prompt"
istrings "github.com/elk-language/go-prompt/strings"
)

var LivePrefix string = ">>> "
Expand All @@ -17,14 +18,18 @@ func executor(in string) {
LivePrefix = in + "> "
}

func completer(in prompt.Document) []prompt.Suggest {
func completer(in prompt.Document) ([]prompt.Suggest, istrings.RuneNumber, istrings.RuneNumber) {
endIndex := in.CurrentRuneIndex()
w := in.GetWordBeforeCursor()
startIndex := endIndex - istrings.RuneCount(w)

s := []prompt.Suggest{
{Text: "users", Description: "Store the username and age"},
{Text: "articles", Description: "Store the article text posted by user"},
{Text: "comments", Description: "Store the text commented to articles"},
{Text: "groups", Description: "Combine users with specific rules"},
}
return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true)
return prompt.FilterHasPrefix(s, w, true), startIndex, endIndex
}

func changeLivePrefix() string {
Expand Down
113 changes: 88 additions & 25 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
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
Expand Down Expand Up @@ -41,8 +42,18 @@ func (b *Buffer) DisplayCursorPosition(columns istrings.Width) Position {
return b.Document().DisplayCursorPosition(columns)
}

// InsertText insert string from current line.
func (b *Buffer) InsertText(text string, overwrite bool, moveCursor bool) {
// Insert string into the buffer and move the cursor.
func (b *Buffer) InsertTextMoveCursor(text string, columns istrings.Width, rows int, overwrite bool) {
b.insertText(text, columns, rows, overwrite, true)
}

// Insert string into the buffer without moving the cursor.
func (b *Buffer) InsertText(text string, overwrite bool) {
b.insertText(text, 0, 0, overwrite, false)
}

// insertText insert string from current line.
func (b *Buffer) insertText(text string, columns istrings.Width, rows int, overwrite bool, moveCursor bool) {
currentTextRunes := []rune(b.Text())
cursor := b.cursorPosition

Expand All @@ -54,22 +65,52 @@ func (b *Buffer) InsertText(text string, overwrite bool, moveCursor bool) {
if i := strings.IndexAny(overwritten, "\n"); i != -1 {
overwritten = overwritten[:i]
}
b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor+istrings.RuneCount(overwritten):]))
b.setText(
string(currentTextRunes[:cursor])+text+string(currentTextRunes[cursor+istrings.RuneCount(overwritten):]),
columns,
rows,
)
} else {
b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor:]))
b.setText(
string(currentTextRunes[:cursor])+text+string(currentTextRunes[cursor:]),
columns,
rows,
)
}

if moveCursor {
b.cursorPosition += istrings.RuneCount(text)
b.RecalculateStartLine(columns, rows)
}
}

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 {
origStartLine := b.startLine
pos := b.DisplayCursorPosition(columns)
if pos.Y > b.startLine+rows-1 {
b.startLine = pos.Y - rows + 1
} else if pos.Y < b.startLine {
b.startLine = pos.Y
}

if b.startLine < 0 {
b.startLine = 0
}
return origStartLine != b.startLine
}

// SetText method to set text and update cursorPosition.
// (When doing this, make sure that the cursor_position is valid for this text.
// text/cursor_position should be consistent at any time, otherwise set a Document instead.)
func (b *Buffer) setText(text string) {
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)
}

// Set cursor position. Return whether it changed.
Expand All @@ -81,40 +122,49 @@ func (b *Buffer) setCursorPosition(p istrings.RuneNumber) {
}
}

func (b *Buffer) setDocument(d *Document) {
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)
b.setText(d.Text, columns, rows)
b.RecalculateStartLine(columns, rows)
}

// CursorLeft move to left on the current line.
func (b *Buffer) CursorLeft(count istrings.RuneNumber) {
// Move to the left on the current line.
// Returns true when the view should be rerendered.
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)
}

// CursorRight move to right on the current line.
func (b *Buffer) CursorRight(count istrings.RuneNumber) {
// Move to the right on the current line.
// Returns true when the view should be rerendered.
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)
}

// CursorUp move cursor to the previous line.
// (for multi-line edit).
func (b *Buffer) CursorUp(count int) {
// 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)
}

// CursorDown move cursor to the next line.
// (for multi-line edit).
func (b *Buffer) CursorDown(count int) {
// 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)
}

// DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text.
func (b *Buffer) DeleteBeforeCursor(count istrings.RuneNumber) (deleted string) {
func (b *Buffer) DeleteBeforeCursor(count istrings.RuneNumber, columns istrings.Width, rows int) (deleted string) {
debug.Assert(count >= 0, "count should be positive")
r := []rune(b.Text())

Expand All @@ -127,28 +177,32 @@ func (b *Buffer) DeleteBeforeCursor(count istrings.RuneNumber) (deleted string)
b.setDocument(&Document{
Text: string(r[:start]) + string(r[b.cursorPosition:]),
cursorPosition: b.cursorPosition - istrings.RuneNumber(len([]rune(deleted))),
})
}, columns, rows)
}
return
}

// NewLine means CR.
func (b *Buffer) NewLine(copyMargin bool) {
func (b *Buffer) NewLine(columns istrings.Width, rows int, copyMargin bool) {
if copyMargin {
b.InsertText("\n"+b.Document().leadingWhitespaceInCurrentLine(), false, true)
b.InsertTextMoveCursor("\n"+b.Document().leadingWhitespaceInCurrentLine(), columns, rows, false)
} else {
b.InsertText("\n", false, true)
b.InsertTextMoveCursor("\n", columns, rows, false)
}
}

// Delete specified number of characters and Return the deleted text.
func (b *Buffer) Delete(count istrings.RuneNumber) string {
func (b *Buffer) Delete(count istrings.RuneNumber, col istrings.Width, row int) string {
r := []rune(b.Text())
if b.cursorPosition < istrings.RuneNumber(len(r)) {
textAfterCursor := b.Document().TextAfterCursor()
textAfterCursorRunes := []rune(textAfterCursor)
deletedRunes := textAfterCursorRunes[:count]
b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+istrings.RuneNumber(len(deletedRunes)):]))
b.setText(
string(r[:b.cursorPosition])+string(r[b.cursorPosition+istrings.RuneNumber(len(deletedRunes)):]),
col,
row,
)

deleted := string(deletedRunes)
return deleted
Expand All @@ -158,21 +212,29 @@ func (b *Buffer) Delete(count istrings.RuneNumber) string {
}

// JoinNextLine joins the next line to the current one by deleting the line ending after the current line.
func (b *Buffer) JoinNextLine(separator string) {
func (b *Buffer) JoinNextLine(separator string, col istrings.Width, row int) {
if !b.Document().OnLastLine() {
b.cursorPosition += b.Document().GetEndOfLinePosition()
b.Delete(1)
b.Delete(1, col, row)
// Remove spaces
b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " "))
b.setText(
b.Document().TextBeforeCursor()+separator+strings.TrimLeft(b.Document().TextAfterCursor(), " "),
col,
row,
)
}
}

// SwapCharactersBeforeCursor swaps the last two characters before the cursor.
func (b *Buffer) SwapCharactersBeforeCursor() {
func (b *Buffer) SwapCharactersBeforeCursor(col istrings.Width, row int) {
if b.cursorPosition >= 2 {
x := b.Text()[b.cursorPosition-2 : b.cursorPosition-1]
y := b.Text()[b.cursorPosition-1 : b.cursorPosition]
b.setText(b.Text()[:b.cursorPosition-2] + y + x + b.Text()[b.cursorPosition:])
b.setText(
b.Text()[:b.cursorPosition-2]+y+x+b.Text()[b.cursorPosition:],
col,
row,
)
}
}

Expand All @@ -181,6 +243,7 @@ func NewBuffer() (b *Buffer) {
b = &Buffer{
workingLines: []string{""},
workingIndex: 0,
startLine: 0,
}
return
}
Loading

0 comments on commit ee09fb5

Please sign in to comment.