Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render using a scrolling buffer #14

Merged
merged 16 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading