diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e35e0c3..8672e82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` @@ -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 @@ -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 diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go index ac82b5f4..0ca21956 100644 --- a/_example/even-lexer/main.go +++ b/_example/even-lexer/main.go @@ -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() { @@ -19,9 +20,7 @@ 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 { @@ -29,7 +28,8 @@ func lexer(line string) []prompt.Token { } 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) } diff --git a/_example/exec-command/main.go b/_example/exec-command/main.go index 02766bf7..703fa52e 100644 --- a/_example/exec-command/main.go +++ b/_example/exec-command/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "os/exec" @@ -9,6 +10,7 @@ import ( func executor(t string) { if t != "bash" { + fmt.Println("Sorry, I don't understand.") return } @@ -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() } diff --git a/_example/http-prompt/main.go b/_example/http-prompt/main.go index d80e0a25..df8e1091 100644 --- a/_example/http-prompt/main.go +++ b/_example/http-prompt/main.go @@ -11,6 +11,7 @@ import ( "strings" prompt "github.com/elk-language/go-prompt" + istrings "github.com/elk-language/go-prompt/strings" ) type RequestContext struct { @@ -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() { diff --git a/_example/live-prefix/main.go b/_example/live-prefix/main.go index 4fe160a5..d4f9cb3c 100644 --- a/_example/live-prefix/main.go +++ b/_example/live-prefix/main.go @@ -4,6 +4,7 @@ import ( "fmt" prompt "github.com/elk-language/go-prompt" + istrings "github.com/elk-language/go-prompt/strings" ) var LivePrefix string = ">>> " @@ -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 { diff --git a/buffer.go b/buffer.go index 0336bd45..4cbbbc5a 100644 --- a/buffer.go +++ b/buffer.go @@ -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 @@ -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 @@ -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. @@ -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()) @@ -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 @@ -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, + ) } } @@ -181,6 +243,7 @@ func NewBuffer() (b *Buffer) { b = &Buffer{ workingLines: []string{""}, workingIndex: 0, + startLine: 0, } return } diff --git a/buffer_test.go b/buffer_test.go index 644a5be7..0db735a0 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -19,7 +19,7 @@ func TestNewBuffer(t *testing.T) { func TestBuffer_InsertText(t *testing.T) { b := NewBuffer() - b.InsertText("some_text", false, true) + b.InsertTextMoveCursor("some_text", DefColCount, DefRowCount, false) if b.Text() != "some_text" { t.Errorf("Text should be %#v, got %#v", "some_text", b.Text()) @@ -32,7 +32,7 @@ func TestBuffer_InsertText(t *testing.T) { func TestBuffer_InsertText_Overwrite(t *testing.T) { b := NewBuffer() - b.InsertText("ABC", false, true) + b.InsertTextMoveCursor("ABC", DefColCount, DefRowCount, false) if b.Text() != "ABC" { t.Errorf("Text should be %#v, got %#v", "ABC", b.Text()) @@ -42,34 +42,34 @@ func TestBuffer_InsertText_Overwrite(t *testing.T) { t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCount("ABC"), b.cursorPosition) } - b.CursorLeft(1) + b.CursorLeft(1, DefColCount, DefRowCount) // Replace C with DEF in ABC - b.InsertText("DEF", true, true) + b.InsertTextMoveCursor("DEF", DefColCount, DefRowCount, true) if b.Text() != "ABDEF" { t.Errorf("Text should be %#v, got %#v", "ABDEF", b.Text()) } - b.CursorLeft(100) + b.CursorLeft(100, DefColCount, DefRowCount) // Replace ABD with GHI in ABDEF - b.InsertText("GHI", true, true) + b.InsertTextMoveCursor("GHI", DefColCount, DefRowCount, true) if b.Text() != "GHIEF" { t.Errorf("Text should be %#v, got %#v", "GHIEF", b.Text()) } - b.CursorLeft(100) + b.CursorLeft(100, DefColCount, DefRowCount) // Replace GHI with J\nK in GHIEF - b.InsertText("J\nK", true, true) + b.InsertTextMoveCursor("J\nK", DefColCount, DefRowCount, true) if b.Text() != "J\nKEF" { t.Errorf("Text should be %#v, got %#v", "J\nKEF", b.Text()) } - b.CursorUp(100) - b.CursorLeft(100) + b.CursorUp(100, DefColCount, DefRowCount) + b.CursorLeft(100, DefColCount, DefRowCount) // Replace J with LMN in J\nKEF test end of line - b.InsertText("LMN", true, true) + b.InsertTextMoveCursor("LMN", DefColCount, DefRowCount, true) if b.Text() != "LMN\nKEF" { t.Errorf("Text should be %#v, got %#v", "LMN\nKEF", b.Text()) @@ -78,12 +78,12 @@ func TestBuffer_InsertText_Overwrite(t *testing.T) { func TestBuffer_CursorMovement(t *testing.T) { b := NewBuffer() - b.InsertText("some_text", false, true) + b.InsertTextMoveCursor("some_text", DefColCount, DefRowCount, false) - b.CursorLeft(1) - b.CursorLeft(2) - b.CursorRight(1) - b.InsertText("A", false, true) + b.CursorLeft(1, DefColCount, DefRowCount) + b.CursorLeft(2, DefColCount, DefRowCount) + b.CursorRight(1, DefColCount, DefRowCount) + b.InsertTextMoveCursor("A", DefColCount, DefRowCount, false) if b.Text() != "some_teAxt" { t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text()) } @@ -92,8 +92,8 @@ func TestBuffer_CursorMovement(t *testing.T) { } // Moving over left character counts. - b.CursorLeft(100) - b.InsertText("A", false, true) + b.CursorLeft(100, DefColCount, DefRowCount) + b.InsertTextMoveCursor("A", DefColCount, DefRowCount, false) if b.Text() != "Asome_teAxt" { t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text()) } @@ -106,12 +106,12 @@ func TestBuffer_CursorMovement(t *testing.T) { func TestBuffer_CursorMovement_WithMultiByte(t *testing.T) { b := NewBuffer() - b.InsertText("あいうえお", false, true) - b.CursorLeft(1) + b.InsertTextMoveCursor("あいうえお", DefColCount, DefRowCount, false) + b.CursorLeft(1, DefColCount, DefRowCount) if l := b.Document().TextAfterCursor(); l != "お" { t.Errorf("Should be 'お', but got %s", l) } - b.InsertText("żółć", true, true) + b.InsertTextMoveCursor("żółć", DefColCount, DefRowCount, true) if b.Text() != "あいうえżółć" { t.Errorf("Text should be %#v, got %#v", "あいうえżółć", b.Text()) } @@ -119,22 +119,22 @@ func TestBuffer_CursorMovement_WithMultiByte(t *testing.T) { func TestBuffer_CursorUp(t *testing.T) { b := NewBuffer() - b.InsertText("long line1\nline2", false, true) - b.CursorUp(1) + b.InsertTextMoveCursor("long line1\nline2", DefColCount, DefRowCount, false) + b.CursorUp(1, DefColCount, DefRowCount) if b.Document().cursorPosition != 5 { t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } // Going up when already at the top. - b.CursorUp(1) + b.CursorUp(1, DefColCount, DefRowCount) if b.Document().cursorPosition != 5 { t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } // Going up to a line that's shorter. - b.setDocument(&Document{}) - b.InsertText("line1\nlong line2", false, true) - b.CursorUp(1) + b.setDocument(&Document{}, DefColCount, DefRowCount) + b.InsertTextMoveCursor("line1\nlong line2", DefColCount, DefRowCount, false) + b.CursorUp(1, DefColCount, DefRowCount) if b.Document().cursorPosition != 5 { t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } @@ -142,20 +142,20 @@ func TestBuffer_CursorUp(t *testing.T) { func TestBuffer_CursorDown(t *testing.T) { b := NewBuffer() - b.InsertText("line1\nline2", false, true) + b.InsertTextMoveCursor("line1\nline2", DefColCount, DefRowCount, false) b.cursorPosition = 3 // Normally going down - b.CursorDown(1) + b.CursorDown(1, DefColCount, DefRowCount) if b.Document().cursorPosition != istrings.RuneCount("line1\nlin") { t.Errorf("Should be %#v, got %#v", istrings.RuneCount("line1\nlin"), b.Document().cursorPosition) } // Going down to a line that's storter. b = NewBuffer() - b.InsertText("long line1\na\nb", false, true) + b.InsertTextMoveCursor("long line1\na\nb", DefColCount, DefRowCount, false) b.cursorPosition = 3 - b.CursorDown(1) + b.CursorDown(1, DefColCount, DefRowCount) if b.Document().cursorPosition != istrings.RuneCount("long line1\na") { t.Errorf("Should be %#v, got %#v", istrings.RuneCount("long line1\na"), b.Document().cursorPosition) } @@ -163,9 +163,9 @@ func TestBuffer_CursorDown(t *testing.T) { func TestBuffer_DeleteBeforeCursor(t *testing.T) { b := NewBuffer() - b.InsertText("some_text", false, true) - b.CursorLeft(2) - deleted := b.DeleteBeforeCursor(1) + b.InsertTextMoveCursor("some_text", DefColCount, DefRowCount, false) + b.CursorLeft(2, DefColCount, DefRowCount) + deleted := b.DeleteBeforeCursor(1, DefColCount, DefRowCount) if b.Text() != "some_txt" { t.Errorf("Should be %#v, got %#v", "some_txt", b.Text()) @@ -178,7 +178,7 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) { } // Delete over the characters length before cursor. - deleted = b.DeleteBeforeCursor(100) + deleted = b.DeleteBeforeCursor(100, DefColCount, DefRowCount) if deleted != "some_t" { t.Errorf("Should be %#v, got %#v", "some_t", deleted) } @@ -187,7 +187,7 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) { } // If cursor position is a beginning of line, it has no effect. - deleted = b.DeleteBeforeCursor(1) + deleted = b.DeleteBeforeCursor(1, DefColCount, DefRowCount) if deleted != "" { t.Errorf("Should be empty, got %#v", deleted) } @@ -195,8 +195,8 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) { func TestBuffer_NewLine(t *testing.T) { b := NewBuffer() - b.InsertText(" hello", false, true) - b.NewLine(false) + b.InsertTextMoveCursor(" hello", DefColCount, DefRowCount, false) + b.NewLine(DefColCount, DefRowCount, false) ac := b.Text() ex := " hello\n" if ac != ex { @@ -204,8 +204,8 @@ func TestBuffer_NewLine(t *testing.T) { } b = NewBuffer() - b.InsertText(" hello", false, true) - b.NewLine(true) + b.InsertTextMoveCursor(" hello", DefColCount, DefRowCount, false) + b.NewLine(DefColCount, DefRowCount, true) ac = b.Text() ex = " hello\n " if ac != ex { @@ -215,9 +215,9 @@ func TestBuffer_NewLine(t *testing.T) { func TestBuffer_JoinNextLine(t *testing.T) { b := NewBuffer() - b.InsertText("line1\nline2\nline3", false, true) - b.CursorUp(1) - b.JoinNextLine(" ") + b.InsertTextMoveCursor("line1\nline2\nline3", DefColCount, DefRowCount, false) + b.CursorUp(1, DefColCount, DefRowCount) + b.JoinNextLine(" ", DefColCount, DefRowCount) ac := b.Text() ex := "line1\nline2 line3" @@ -227,9 +227,9 @@ func TestBuffer_JoinNextLine(t *testing.T) { // Test when there is no '\n' in the text b = NewBuffer() - b.InsertText("line1", false, true) + b.InsertTextMoveCursor("line1", DefColCount, DefRowCount, false) b.cursorPosition = 0 - b.JoinNextLine(" ") + b.JoinNextLine(" ", DefColCount, DefRowCount) ac = b.Text() ex = "line1" if ac != ex { @@ -239,9 +239,9 @@ func TestBuffer_JoinNextLine(t *testing.T) { func TestBuffer_SwapCharactersBeforeCursor(t *testing.T) { b := NewBuffer() - b.InsertText("hello world", false, true) - b.CursorLeft(2) - b.SwapCharactersBeforeCursor() + b.InsertTextMoveCursor("hello world", DefColCount, DefRowCount, false) + b.CursorLeft(2, DefColCount, DefRowCount) + b.SwapCharactersBeforeCursor(DefColCount, DefRowCount) ac := b.Text() ex := "hello wrold" if ac != ex { diff --git a/completion.go b/completion.go index 1eabd56a..50953e67 100644 --- a/completion.go +++ b/completion.go @@ -16,12 +16,6 @@ const ( rightSuffix = " " ) -var ( - leftMargin = istrings.GetWidth(leftPrefix + leftSuffix) - rightMargin = istrings.GetWidth(rightPrefix + rightSuffix) - completionMargin = leftMargin + rightMargin -) - // Suggest represents a single suggestion // in the auto-complete box. type Suggest struct { @@ -31,10 +25,13 @@ type Suggest struct { // CompletionManager manages which suggestion is now selected. type CompletionManager struct { - selected int // -1 means nothing is selected. - tmp []Suggest - max uint16 - completer Completer + selected int // -1 means nothing is selected. + tmp []Suggest + max uint16 + completer Completer + startCharIndex istrings.RuneNumber // index of the first char of the text that should be replaced by the selected suggestion + endCharIndex istrings.RuneNumber // index of the last char of the text that should be replaced by the selected suggestion + shouldUpdate bool verticalScroll int wordSeparator string @@ -68,7 +65,7 @@ func (c *CompletionManager) Reset() { // Update the suggestions. func (c *CompletionManager) Update(in Document) { - c.tmp = c.completer(in) + c.tmp, c.startCharIndex, c.endCharIndex = c.completer(in) } // Select the previous suggestion item. @@ -81,12 +78,13 @@ func (c *CompletionManager) Previous() { } // Next to select the next suggestion item. -func (c *CompletionManager) Next() { +func (c *CompletionManager) Next() int { if c.verticalScroll+int(c.max)-1 == c.selected { c.verticalScroll++ } c.selected++ c.update() + return c.selected } // Completing returns true when the CompletionManager selects something. @@ -101,7 +99,8 @@ func (c *CompletionManager) update() { } if c.selected >= len(c.tmp) { - c.Reset() + c.selected = -1 + c.verticalScroll = 0 } else if c.selected < -1 { c.selected = len(c.tmp) - 1 c.verticalScroll = len(c.tmp) - max @@ -211,6 +210,6 @@ var _ Completer = NoopCompleter // NoopCompleter implements a Completer function // that always returns no suggestions. -func NoopCompleter(_ Document) []Suggest { - return nil +func NoopCompleter(_ Document) ([]Suggest, istrings.RuneNumber, istrings.RuneNumber) { + return nil, 0, 0 } diff --git a/completion_test.go b/completion_test.go index 959d86f1..11bc66f2 100644 --- a/completion_test.go +++ b/completion_test.go @@ -208,7 +208,8 @@ func TestFormatText(t *testing.T) { } func TestNoopCompleter(t *testing.T) { - if NoopCompleter(Document{}) != nil { + sug, start, end := NoopCompleter(Document{}) + if sug != nil || start != 0 || end != 0 { t.Errorf("NoopCompleter should return nil") } } diff --git a/constructor.go b/constructor.go index 843f524c..f6d32c6d 100644 --- a/constructor.go +++ b/constructor.go @@ -80,7 +80,7 @@ func WithPrefix(prefix string) Option { // WithInitialText can be used to set the initial buffer text. func WithInitialText(text string) Option { return func(p *Prompt) error { - p.buf.InsertText(text, false, true) + p.Buffer.InsertTextMoveCursor(text, p.renderer.col, int(p.renderer.row), true) return nil } } @@ -133,22 +133,6 @@ func WithInputBGColor(x Color) Option { } } -// WithPreviewSuggestionTextColor to change a text color which is completed -func WithPreviewSuggestionTextColor(x Color) Option { - return func(p *Prompt) error { - p.renderer.previewSuggestionTextColor = x - return nil - } -} - -// WithPreviewSuggestionBGColor to change a background color which is completed -func WithPreviewSuggestionBGColor(x Color) Option { - return func(p *Prompt) error { - p.renderer.previewSuggestionBGColor = x - return nil - } -} - // WithSuggestionTextColor to change a text color in drop down suggestions. func WithSuggestionTextColor(x Color) Option { return func(p *Prompt) error { @@ -315,7 +299,7 @@ func New(executor Executor, opts ...Option) *Prompt { pt := &Prompt{ reader: NewStdinReader(), renderer: NewRenderer(), - buf: NewBuffer(), + Buffer: NewBuffer(), executor: executor, history: NewHistory(), completion: NewCompletionManager(6), diff --git a/document.go b/document.go index 7cb0f213..a3ae1ff8 100644 --- a/document.go +++ b/document.go @@ -56,6 +56,11 @@ func (d *Document) GetCharRelativeToCursor(offset istrings.RuneNumber) (r rune) return 0 } +// Returns the index of the rune that's under the cursor. +func (d *Document) CurrentRuneIndex() istrings.RuneNumber { + return d.cursorPosition +} + // TextBeforeCursor returns the text before the cursor. func (d *Document) TextBeforeCursor() string { r := []rune(d.Text) diff --git a/emacs.go b/emacs.go index 4770a360..3314d273 100644 --- a/emacs.go +++ b/emacs.go @@ -45,95 +45,127 @@ var emacsKeyBindings = []KeyBind{ // Go to the End of the line { Key: ControlE, - Fn: func(buf *Buffer) { - buf.CursorRight(istrings.RuneCount(buf.Document().CurrentLineAfterCursor())) + Fn: func(p *Prompt) bool { + return p.CursorRight( + istrings.RuneCount(p.Buffer.Document().CurrentLineAfterCursor()), + ) }, }, // Go to the beginning of the line { Key: ControlA, - Fn: func(buf *Buffer) { - buf.CursorLeft(buf.Document().FindStartOfFirstWordOfLine()) + Fn: func(p *Prompt) bool { + return p.CursorLeft( + p.Buffer.Document().FindStartOfFirstWordOfLine(), + ) }, }, // Cut the Line after the cursor { Key: ControlK, - Fn: func(buf *Buffer) { - buf.Delete(istrings.RuneCount(buf.Document().CurrentLineAfterCursor())) + Fn: func(p *Prompt) bool { + p.Buffer.Delete( + istrings.RuneCount(p.Buffer.Document().CurrentLineAfterCursor()), + p.renderer.col, + p.renderer.row, + ) + return true }, }, // Cut/delete the Line before the cursor { Key: ControlU, - Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().CurrentLineBeforeCursor())) + Fn: func(p *Prompt) bool { + p.Buffer.DeleteBeforeCursor( + istrings.RuneCount(p.Buffer.Document().CurrentLineBeforeCursor()), + p.renderer.col, + p.renderer.row, + ) + return true }, }, // Delete character under the cursor { Key: ControlD, - Fn: func(buf *Buffer) { - if buf.Text() != "" { - buf.Delete(1) + Fn: func(p *Prompt) bool { + if p.Buffer.Text() != "" { + p.Buffer.Delete(1, p.renderer.col, p.renderer.row) + return true } + return false }, }, // Backspace { Key: ControlH, - Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(1) + Fn: func(p *Prompt) bool { + p.Buffer.DeleteBeforeCursor(1, p.renderer.col, p.renderer.row) + return true }, }, // Right allow: Forward one character { Key: ControlF, - Fn: func(buf *Buffer) { - buf.CursorRight(1) + Fn: func(p *Prompt) bool { + return p.CursorRight(1) }, }, // Alt Right allow: Forward one word { Key: AltRight, - Fn: func(buf *Buffer) { - buf.CursorRight(buf.Document().FindRuneNumberUntilEndOfCurrentWord()) + Fn: func(p *Prompt) bool { + return p.CursorRight( + p.Buffer.Document().FindRuneNumberUntilEndOfCurrentWord(), + ) }, }, // Left allow: Backward one character { Key: ControlB, - Fn: func(buf *Buffer) { - buf.CursorLeft(1) + Fn: func(p *Prompt) bool { + return p.CursorLeft(1) }, }, // Alt Left allow: Backward one word { Key: AltLeft, - Fn: func(buf *Buffer) { - buf.CursorLeft(buf.Document().FindRuneNumberUntilStartOfPreviousWord()) + Fn: func(p *Prompt) bool { + return p.CursorLeft( + p.Buffer.Document().FindRuneNumberUntilStartOfPreviousWord(), + ) }, }, // Cut the Word before the cursor. { Key: ControlW, - Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().GetWordBeforeCursorWithSpace())) + Fn: func(p *Prompt) bool { + p.Buffer.DeleteBeforeCursor( + istrings.RuneCount(p.Buffer.Document().GetWordBeforeCursorWithSpace()), + p.renderer.col, + p.renderer.row, + ) + return true }, }, { Key: AltBackspace, - Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().GetWordBeforeCursorWithSpace())) + Fn: func(p *Prompt) bool { + p.Buffer.DeleteBeforeCursor( + istrings.RuneCount(p.Buffer.Document().GetWordBeforeCursorWithSpace()), + p.renderer.col, + p.renderer.row, + ) + return true }, }, // Clear the Screen, similar to the clear command { Key: ControlL, - Fn: func(buf *Buffer) { + Fn: func(p *Prompt) bool { consoleWriter.EraseScreen() consoleWriter.CursorGoTo(0, 0) debug.AssertNoError(consoleWriter.Flush()) + return true }, }, } diff --git a/emacs_test.go b/emacs_test.go index 605c3bd8..69adba2d 100644 --- a/emacs_test.go +++ b/emacs_test.go @@ -7,30 +7,31 @@ import ( ) func TestEmacsKeyBindings(t *testing.T) { - buf := NewBuffer() - buf.InsertText("abcde", false, true) + p := New(NoopExecutor) + buf := p.Buffer + buf.InsertTextMoveCursor("abcde", DefColCount, DefRowCount, false) if buf.cursorPosition != istrings.RuneNumber(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } // Go to the beginning of the line - applyEmacsKeyBind(buf, ControlA) + applyEmacsKeyBind(p, ControlA) if buf.cursorPosition != 0 { t.Errorf("Want %d, but got %d", 0, buf.cursorPosition) } // Go to the end of the line - applyEmacsKeyBind(buf, ControlE) + applyEmacsKeyBind(p, ControlE) if buf.cursorPosition != istrings.RuneNumber(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } } -func applyEmacsKeyBind(buf *Buffer, key Key) { +func applyEmacsKeyBind(p *Prompt, key Key) { for i := range emacsKeyBindings { kb := emacsKeyBindings[i] if kb.Key == key { - kb.Fn(buf) + kb.Fn(p) } } } diff --git a/history.go b/history.go index 9bf68d19..cb0034e3 100644 --- a/history.go +++ b/history.go @@ -1,5 +1,9 @@ package prompt +import ( + istrings "github.com/elk-language/go-prompt/strings" +) + // History stores the texts that are entered. type History struct { histories []string @@ -23,7 +27,7 @@ func (h *History) Clear() { // Older saves a buffer of current line and get a buffer of previous line by up-arrow. // The changes of line buffers are stored until new history is created. -func (h *History) Older(buf *Buffer) (new *Buffer, changed bool) { +func (h *History) Older(buf *Buffer, columns istrings.Width, rows int) (new *Buffer, changed bool) { if len(h.tmp) == 1 || h.selected == 0 { return buf, false } @@ -31,13 +35,13 @@ func (h *History) Older(buf *Buffer) (new *Buffer, changed bool) { h.selected-- new = NewBuffer() - new.InsertText(h.tmp[h.selected], false, true) + new.InsertTextMoveCursor(h.tmp[h.selected], columns, rows, false) return new, true } // Newer saves a buffer of current line and get a buffer of next line by up-arrow. // The changes of line buffers are stored until new history is created. -func (h *History) Newer(buf *Buffer) (new *Buffer, changed bool) { +func (h *History) Newer(buf *Buffer, columns istrings.Width, rows int) (new *Buffer, changed bool) { if h.selected >= len(h.tmp)-1 { return buf, false } @@ -45,7 +49,7 @@ func (h *History) Newer(buf *Buffer) (new *Buffer, changed bool) { h.selected++ new = NewBuffer() - new.InsertText(h.tmp[h.selected], false, true) + new.InsertTextMoveCursor(h.tmp[h.selected], columns, rows, false) return new, true } diff --git a/history_test.go b/history_test.go index bf638699..cda517bb 100644 --- a/history_test.go +++ b/history_test.go @@ -38,10 +38,10 @@ func TestHistoryOlder(t *testing.T) { // Prepare buffer buf := NewBuffer() - buf.InsertText("echo 2", false, true) + buf.InsertTextMoveCursor("echo 2", DefColCount, DefRowCount, false) // [1 time] Call Older function - buf1, changed := h.Older(buf) + buf1, changed := h.Older(buf, DefColCount, DefRowCount) if !changed { t.Error("Should be changed history but not changed.") } @@ -51,8 +51,8 @@ func TestHistoryOlder(t *testing.T) { // [2 times] Call Older function buf = NewBuffer() - buf.InsertText("echo 1", false, true) - buf2, changed := h.Older(buf) + buf.InsertTextMoveCursor("echo 1", DefColCount, DefRowCount, false) + buf2, changed := h.Older(buf, DefColCount, DefRowCount) if changed { t.Error("Should be not changed history but changed.") } diff --git a/key_bind.go b/key_bind.go index 0332e27d..7bd7c0fc 100644 --- a/key_bind.go +++ b/key_bind.go @@ -1,7 +1,7 @@ package prompt // KeyBindFunc receives buffer and processed it. -type KeyBindFunc func(*Buffer) +type KeyBindFunc func(p *Prompt) (rerender bool) // KeyBind represents which key should do what operation. type KeyBind struct { diff --git a/key_bind_func.go b/key_bind_func.go index 7b49f713..6f604adf 100644 --- a/key_bind_func.go +++ b/key_bind_func.go @@ -5,33 +5,35 @@ import ( ) // GoLineEnd Go to the End of the line -func GoLineEnd(buf *Buffer) { - x := []rune(buf.Document().TextAfterCursor()) - buf.CursorRight(istrings.RuneNumber(len(x))) +func GoLineEnd(p *Prompt) bool { + x := []rune(p.Buffer.Document().TextAfterCursor()) + return p.CursorRight(istrings.RuneNumber(len(x))) } // GoLineBeginning Go to the beginning of the line -func GoLineBeginning(buf *Buffer) { - x := []rune(buf.Document().TextBeforeCursor()) - buf.CursorLeft(istrings.RuneNumber(len(x))) +func GoLineBeginning(p *Prompt) bool { + x := []rune(p.Buffer.Document().TextBeforeCursor()) + return p.CursorLeft(istrings.RuneNumber(len(x))) } // DeleteChar Delete character under the cursor -func DeleteChar(buf *Buffer) { - buf.Delete(1) +func DeleteChar(p *Prompt) bool { + p.Buffer.Delete(1, p.renderer.col, p.renderer.row) + return true } // DeleteBeforeChar Go to Backspace -func DeleteBeforeChar(buf *Buffer) { - buf.DeleteBeforeCursor(1) +func DeleteBeforeChar(p *Prompt) bool { + p.Buffer.DeleteBeforeCursor(1, p.renderer.col, p.renderer.row) + return true } // GoRightChar Forward one character -func GoRightChar(buf *Buffer) { - buf.CursorRight(1) +func GoRightChar(p *Prompt) bool { + return p.CursorRight(1) } // GoLeftChar Backward one character -func GoLeftChar(buf *Buffer) { - buf.CursorLeft(1) +func GoLeftChar(p *Prompt) bool { + return p.CursorLeft(1) } diff --git a/lexer.go b/lexer.go index fba81987..038da6bf 100644 --- a/lexer.go +++ b/lexer.go @@ -1,5 +1,9 @@ package prompt +import ( + istrings "github.com/elk-language/go-prompt/strings" +) + // Lexer is a streaming lexer that takes in a piece of text // and streams tokens with the Next() method type Lexer interface { @@ -11,21 +15,21 @@ type Lexer interface { // Token is a single unit of text returned by a Lexer. type Token interface { - Color() Color - Lexeme() string // original string that matches this token + Color() Color // Color of the token + LastByteIndex() istrings.ByteNumber // Index of the last byte of this token } // SimpleToken as the default implementation of Token. type SimpleToken struct { - color Color - lexeme string + color Color + lastByteIndex istrings.ByteNumber } // Create a new SimpleToken. -func NewSimpleToken(color Color, lexeme string) *SimpleToken { +func NewSimpleToken(color Color, index istrings.ByteNumber) *SimpleToken { return &SimpleToken{ - color: color, - lexeme: lexeme, + color: color, + lastByteIndex: index, } } @@ -35,8 +39,8 @@ func (t *SimpleToken) Color() Color { } // Retrieve the text that this token represents. -func (t *SimpleToken) Lexeme() string { - return t.lexeme +func (t *SimpleToken) LastByteIndex() istrings.ByteNumber { + return t.lastByteIndex } // LexerFunc is a function implementing diff --git a/lexer_test.go b/lexer_test.go index b4ccd567..8b14f481 100644 --- a/lexer_test.go +++ b/lexer_test.go @@ -3,6 +3,7 @@ package prompt import ( "testing" + istrings "github.com/elk-language/go-prompt/strings" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -16,32 +17,32 @@ func TestEagerLexerNext(t *testing.T) { "return the first token when at the beginning": { lexer: &EagerLexer{ tokens: []Token{ - &SimpleToken{lexeme: "foo"}, - &SimpleToken{lexeme: "bar"}, + &SimpleToken{lastByteIndex: 0}, + &SimpleToken{lastByteIndex: 1}, }, currentIndex: 0, }, - want: &SimpleToken{lexeme: "foo"}, + want: &SimpleToken{lastByteIndex: 0}, ok: true, }, "return the second token": { lexer: &EagerLexer{ tokens: []Token{ - &SimpleToken{lexeme: "foo"}, - &SimpleToken{lexeme: "bar"}, - &SimpleToken{lexeme: "baz"}, + &SimpleToken{lastByteIndex: 3}, + &SimpleToken{lastByteIndex: 5}, + &SimpleToken{lastByteIndex: 6}, }, currentIndex: 1, }, - want: &SimpleToken{lexeme: "bar"}, + want: &SimpleToken{lastByteIndex: 5}, ok: true, }, "return false when at the end": { lexer: &EagerLexer{ tokens: []Token{ - &SimpleToken{lexeme: "foo"}, - &SimpleToken{lexeme: "bar"}, - &SimpleToken{lexeme: "baz"}, + &SimpleToken{lastByteIndex: 0}, + &SimpleToken{lastByteIndex: 4}, + &SimpleToken{lastByteIndex: 5}, }, currentIndex: 3, }, @@ -68,8 +69,8 @@ func TestEagerLexerNext(t *testing.T) { func charLex(s string) []Token { var result []Token - for _, char := range s { - result = append(result, NewSimpleToken(0, string(char))) + for i := range s { + result = append(result, NewSimpleToken(0, istrings.ByteNumber(i))) } return result @@ -85,18 +86,18 @@ func TestEagerLexerInit(t *testing.T) { lexer: &EagerLexer{ lexFunc: charLex, tokens: []Token{ - &SimpleToken{lexeme: "foo"}, - &SimpleToken{lexeme: "bar"}, + &SimpleToken{lastByteIndex: 2}, + &SimpleToken{lastByteIndex: 10}, }, - currentIndex: 2, + currentIndex: 11, }, input: "foo", want: &EagerLexer{ lexFunc: charLex, tokens: []Token{ - &SimpleToken{lexeme: "f"}, - &SimpleToken{lexeme: "o"}, - &SimpleToken{lexeme: "o"}, + &SimpleToken{lastByteIndex: 0}, + &SimpleToken{lastByteIndex: 1}, + &SimpleToken{lastByteIndex: 2}, }, currentIndex: 0, }, diff --git a/position.go b/position.go index 14ef66f2..138e9243 100644 --- a/position.go +++ b/position.go @@ -47,13 +47,18 @@ func (p Position) Subtract(other Position) Position { // positionAtEndOfString calculates the position of the // p at the end of the given string. +func positionAtEndOfStringLine(str string, columns istrings.Width, line int) Position { + return positionAtEndOfReaderLine(strings.NewReader(str), columns, line) +} + +// positionAtEndOfString calculates the position +// at the end of the given string. func positionAtEndOfString(str string, columns istrings.Width) Position { - pos := positionAtEndOfReader(strings.NewReader(str), columns) - return pos + return positionAtEndOfReader(strings.NewReader(str), columns) } -// positionAtEndOfReader calculates the position of the -// p at the end of the given io.Reader. +// positionAtEndOfReader calculates the position +// at the end of the given io.Reader. func positionAtEndOfReader(reader io.RuneReader, columns istrings.Width) Position { var down int var right istrings.Width @@ -93,3 +98,55 @@ charLoop: Y: down, } } + +// positionAtEndOfReaderLine calculates the position +// at the given line of the given io.Reader. +func positionAtEndOfReaderLine(reader io.RuneReader, columns istrings.Width, line int) Position { + var down int + var right istrings.Width + +charLoop: + for { + char, _, err := reader.ReadRune() + if err != nil { + break charLoop + } + + switch char { + case '\r': + char, _, err := reader.ReadRune() + if err != nil { + break charLoop + } + + if char == '\n' { + if down == line { + break charLoop + } + down++ + right = 0 + } + case '\n': + if down == line { + break charLoop + } + down++ + right = 0 + default: + right += istrings.Width(runewidth.RuneWidth(char)) + if right > columns { + if down == line { + right = columns - 1 + break charLoop + } + right = istrings.Width(runewidth.RuneWidth(char)) + down++ + } + } + } + + return Position{ + X: right, + Y: down, + } +} diff --git a/position_test.go b/position_test.go index ce2ed7f4..0ea6dd31 100644 --- a/position_test.go +++ b/position_test.go @@ -76,6 +76,59 @@ func TestPositionAtEndOfString(t *testing.T) { } } +func TestPositionAtEndOfStringLine(t *testing.T) { + tests := map[string]struct { + input string + cols istrings.Width + line int + want Position + }{ + "last line overflows": { + input: `hi +foobar`, + cols: 3, + line: 1, + want: Position{ + X: 2, + Y: 1, + }, + }, + "last line is in the middle": { + input: `hi +foo hey +bar boo ba +baz`, + cols: 20, + line: 2, + want: Position{ + X: 10, + Y: 2, + }, + }, + "last line is out of bounds": { + input: `hi +foo hey +bar boo ba +baz`, + cols: 20, + line: 20, + want: Position{ + X: 3, + Y: 3, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := positionAtEndOfStringLine(tc.input, tc.cols, tc.line) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf(diff) + } + }) + } +} + func TestPositionAdd(t *testing.T) { tests := map[string]struct { left Position diff --git a/prompt.go b/prompt.go index f5c0fd2d..615ce15a 100644 --- a/prompt.go +++ b/prompt.go @@ -37,12 +37,15 @@ type ExecuteOnEnterCallback func(input string, indentSize int) (indent int, exec // Completer is a function that returns // a slice of suggestions for the given Document. -type Completer func(Document) []Suggest +// +// startChar and endChar represent the indices of the first and last rune of the text +// that the suggestions were generated for and that should be replaced by the selected suggestion. +type Completer func(Document) (suggestions []Suggest, startChar, endChar istrings.RuneNumber) // Prompt is a core struct of go-prompt. type Prompt struct { reader Reader - buf *Buffer + Buffer *Buffer renderer *Renderer executor Executor history *History @@ -55,6 +58,7 @@ type Prompt struct { exitChecker ExitChecker executeOnEnterCallback ExecuteOnEnterCallback skipClose bool + completionReset bool } // UserInput is the struct that contains the user input context. @@ -71,10 +75,10 @@ func (p *Prompt) Run() { defer p.Close() if p.completion.showAtStart { - p.completion.Update(*p.buf.Document()) + p.completion.Update(*p.Buffer.Document()) } - p.renderer.Render(p.buf, p.completion, p.lexer) + p.renderer.Render(p.Buffer, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) @@ -88,12 +92,12 @@ func (p *Prompt) Run() { for { select { case b := <-bufCh: - if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf, p.lexer) + if shouldExit, rerender, input := p.feed(b); shouldExit { + p.renderer.BreakLine(p.Buffer, p.lexer) stopReadBufCh <- struct{}{} stopHandleSignalCh <- struct{}{} return - } else if e != nil { + } else if input != nil { // Stop goroutine to run readBuffer function stopReadBufCh <- struct{}{} stopHandleSignalCh <- struct{}{} @@ -101,13 +105,13 @@ func (p *Prompt) Run() { // Unset raw mode // Reset to Blocking mode because returned EAGAIN when still set non-blocking mode. debug.AssertNoError(p.reader.Close()) - p.executor(e.input) + p.executor(input.input) - p.completion.Update(*p.buf.Document()) + p.completion.Update(*p.Buffer.Document()) - p.renderer.Render(p.buf, p.completion, p.lexer) + p.renderer.Render(p.Buffer, p.completion, p.lexer) - if p.exitChecker != nil && p.exitChecker(e.input, true) { + if p.exitChecker != nil && p.exitChecker(input.input, true) { p.skipClose = true return } @@ -115,15 +119,19 @@ func (p *Prompt) Run() { debug.AssertNoError(p.reader.Open()) go p.readBuffer(bufCh, stopReadBufCh) go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh) - } else { - p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion, p.lexer) + } else if rerender { + if p.completion.shouldUpdate { + p.completion.Update(*p.Buffer.Document()) + } + p.renderer.Render(p.Buffer, p.completion, p.lexer) } case w := <-winSizeCh: p.renderer.UpdateWinSize(w) - p.renderer.Render(p.buf, p.completion, p.lexer) + 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.buf, p.lexer) + p.renderer.BreakLine(p.Buffer, p.lexer) p.Close() os.Exit(code) default: @@ -138,109 +146,135 @@ func Log(format string, a ...any) { log.Fatalf("error opening file: %v", err) } defer f.Close() - fmt.Fprintf(f, format, a...) + fmt.Fprintf(f, format+"\n", a...) } -func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { +func (p *Prompt) feed(b []byte) (shouldExit bool, rerender bool, userInput *UserInput) { key := GetKey(b) - p.buf.lastKeyStroke = key + p.Buffer.lastKeyStroke = key // completion completing := p.completion.Completing() - p.handleCompletionKeyBinding(b, key, completing) + if p.handleCompletionKeyBinding(b, key, completing) { + return false, true, nil + } + + cols := p.renderer.UserInputColumns() + rows := p.renderer.row switch key { case Enter, ControlJ, ControlM: - indent, execute := p.executeOnEnterCallback(p.buf.Text(), p.renderer.indentSize) + indent, execute := p.executeOnEnterCallback(p.Buffer.Text(), p.renderer.indentSize) if !execute { - p.buf.NewLine(false) + p.Buffer.NewLine(cols, rows, false) + var indentStrBuilder strings.Builder indentUnitCount := indent * p.renderer.indentSize for i := 0; i < indentUnitCount; i++ { indentStrBuilder.WriteRune(IndentUnit) } - p.buf.InsertText(indentStrBuilder.String(), false, true) + p.Buffer.InsertTextMoveCursor(indentStrBuilder.String(), cols, rows, false) break } - p.renderer.BreakLine(p.buf, p.lexer) - userInput = &UserInput{input: p.buf.Text()} - p.buf = NewBuffer() + p.renderer.BreakLine(p.Buffer, p.lexer) + userInput = &UserInput{input: p.Buffer.Text()} + p.Buffer = NewBuffer() if userInput.input != "" { p.history.Add(userInput.input) } case ControlC: - p.renderer.BreakLine(p.buf, p.lexer) - p.buf = NewBuffer() + p.renderer.BreakLine(p.Buffer, p.lexer) + p.Buffer = NewBuffer() p.history.Clear() case Up, ControlP: - line := p.buf.Document().CursorPositionRow() + line := p.Buffer.Document().CursorPositionRow() if line > 0 { - p.buf.CursorUp(1) - break + rerender = p.CursorUp(1) + return false, rerender, nil } if completing { break } - if newBuf, changed := p.history.Older(p.buf); changed { - p.buf = newBuf + if newBuf, changed := p.history.Older(p.Buffer, cols, rows); changed { + p.Buffer = newBuf } case Down, ControlN: - endOfTextRow := p.buf.Document().TextEndPositionRow() - row := p.buf.Document().CursorPositionRow() + endOfTextRow := p.Buffer.Document().TextEndPositionRow() + row := p.Buffer.Document().CursorPositionRow() if endOfTextRow > row { - p.buf.CursorDown(1) - break + rerender = p.CursorDown(1) + return false, rerender, nil } if completing { break } - if newBuf, changed := p.history.Newer(p.buf); changed { - p.buf = newBuf + if newBuf, changed := p.history.Newer(p.Buffer, cols, rows); changed { + p.Buffer = newBuf } - return + return false, true, nil case ControlD: - if p.buf.Text() == "" { - shouldExit = true - return + if p.Buffer.Text() == "" { + return true, true, nil } case NotDefined: - if p.handleASCIICodeBinding(b) { - return + var checked bool + checked, rerender = p.handleASCIICodeBinding(b, cols, rows) + + if checked { + return false, rerender, nil } char, _ := utf8.DecodeRune(b) if unicode.IsControl(char) { - return + return false, false, nil } - p.buf.InsertText(string(b), false, true) + p.Buffer.InsertTextMoveCursor(string(b), cols, rows, false) } - shouldExit = p.handleKeyBinding(key) - return + shouldExit, rerender = p.handleKeyBinding(key, cols, rows) + return shouldExit, rerender, userInput } -func (p *Prompt) handleCompletionKeyBinding(b []byte, key Key, completing bool) { +func (p *Prompt) handleCompletionKeyBinding(b []byte, key Key, completing bool) (handled bool) { + p.completion.shouldUpdate = true + cols := p.renderer.UserInputColumns() + rows := p.renderer.row + completionLen := len(p.completion.tmp) + p.completionReset = false + keySwitch: switch key { case Down: if completing || p.completionOnDown { - p.completion.Next() + p.updateSuggestions(func() { + p.completion.Next() + }) + return true } case ControlI: - p.completion.Next() + p.updateSuggestions(func() { + p.completion.Next() + }) + return true case Up: if completing { - p.completion.Previous() + p.updateSuggestions(func() { + p.completion.Previous() + }) + return true } case Tab: - if len(p.completion.GetSuggestions()) > 0 { + if completionLen > 0 { // If there are any suggestions, select the next one - p.completion.Next() - break + p.updateSuggestions(func() { + p.completion.Next() + }) + + return true } // if there are no suggestions insert indentation @@ -255,39 +289,95 @@ keySwitch: newBytes = append(newBytes, byt) } } - p.buf.InsertText(string(newBytes), false, true) + p.Buffer.InsertTextMoveCursor(string(newBytes), cols, rows, false) + return true case BackTab: - if len(p.completion.GetSuggestions()) > 0 { + if completionLen > 0 { // If there are any suggestions, select the previous one - p.completion.Previous() - break + p.updateSuggestions(func() { + p.completion.Previous() + }) + return true } - text := p.buf.Document().CurrentLineBeforeCursor() + text := p.Buffer.Document().CurrentLineBeforeCursor() for _, char := range text { if char != IndentUnit { break keySwitch } } - p.buf.DeleteBeforeCursor(istrings.RuneNumber(p.renderer.indentSize)) + p.Buffer.DeleteBeforeCursor(istrings.RuneNumber(p.renderer.indentSize), cols, rows) + return true default: if s, ok := p.completion.GetSelectedSuggestion(); ok { - w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator) + w := p.Buffer.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator) if w != "" { - p.buf.DeleteBeforeCursor(istrings.RuneNumber(len([]rune(w)))) + p.Buffer.DeleteBeforeCursor(istrings.RuneNumber(len([]rune(w))), cols, rows) } - p.buf.InsertText(s.Text, false, true) + p.Buffer.InsertTextMoveCursor(s.Text, cols, rows, false) + } + if completionLen > 0 { + p.completionReset = true } p.completion.Reset() } + return false } -func (p *Prompt) handleKeyBinding(key Key) bool { - shouldExit := false +func (p *Prompt) updateSuggestions(fn func()) { + cols := p.renderer.UserInputColumns() + rows := p.renderer.row + + prevStart := p.completion.startCharIndex + prevEnd := p.completion.endCharIndex + prevSuggestion, prevSelected := p.completion.GetSelectedSuggestion() + + fn() + + p.completion.shouldUpdate = false + newSuggestion, newSelected := p.completion.GetSelectedSuggestion() + + // do nothing + if !prevSelected && !newSelected { + return + } + + // insert the new selection + if !prevSelected { + p.Buffer.DeleteBeforeCursor(p.completion.endCharIndex-p.completion.startCharIndex, cols, rows) + p.Buffer.InsertTextMoveCursor(newSuggestion.Text, cols, rows, false) + return + } + // delete the previous selection + if !newSelected { + p.Buffer.DeleteBeforeCursor( + istrings.RuneCount(prevSuggestion.Text)-(prevEnd-prevStart), + cols, + rows, + ) + return + } + + // delete previous selection and render the new one + p.Buffer.DeleteBeforeCursor( + istrings.RuneCount(prevSuggestion.Text), + cols, + rows, + ) + + p.Buffer.InsertTextMoveCursor(newSuggestion.Text, cols, rows, false) +} + +func (p *Prompt) handleKeyBinding(key Key, cols istrings.Width, rows int) (shouldExit bool, rerender bool) { + var executed bool for i := range commonKeyBindings { kb := commonKeyBindings[i] if kb.Key == key { - kb.Fn(p.buf) + result := kb.Fn(p) + executed = true + if !rerender { + rerender = result + } } } @@ -296,7 +386,11 @@ func (p *Prompt) handleKeyBinding(key Key) bool { for i := range emacsKeyBindings { kb := emacsKeyBindings[i] if kb.Key == key { - kb.Fn(p.buf) + result := kb.Fn(p) + executed = true + if !rerender { + rerender = result + } } } } @@ -305,24 +399,33 @@ func (p *Prompt) handleKeyBinding(key Key) bool { for i := range p.keyBindings { kb := p.keyBindings[i] if kb.Key == key { - kb.Fn(p.buf) + result := kb.Fn(p) + executed = true + if !rerender { + rerender = result + } } } - if p.exitChecker != nil && p.exitChecker(p.buf.Text(), false) { + if p.exitChecker != nil && p.exitChecker(p.Buffer.Text(), false) { shouldExit = true } - return shouldExit + if !executed && !rerender { + rerender = true + } + return shouldExit, rerender } -func (p *Prompt) handleASCIICodeBinding(b []byte) bool { - checked := false +func (p *Prompt) handleASCIICodeBinding(b []byte, cols istrings.Width, rows int) (checked, rerender bool) { for _, kb := range p.ASCIICodeBindings { if bytes.Equal(kb.ASCIICode, b) { - kb.Fn(p.buf) + result := kb.Fn(p) + if !rerender { + rerender = result + } checked = true } } - return checked + return checked, rerender } // Input starts the prompt, lets the user @@ -334,10 +437,10 @@ func (p *Prompt) Input() string { defer p.Close() if p.completion.showAtStart { - p.completion.Update(*p.buf.Document()) + p.completion.Update(*p.Buffer.Document()) } - p.renderer.Render(p.buf, p.completion, p.lexer) + p.renderer.Render(p.Buffer, p.completion, p.lexer) bufCh := make(chan []byte, 128) stopReadBufCh := make(chan struct{}) go p.readBuffer(bufCh, stopReadBufCh) @@ -345,17 +448,17 @@ func (p *Prompt) Input() string { for { select { case b := <-bufCh: - if shouldExit, e := p.feed(b); shouldExit { - p.renderer.BreakLine(p.buf, p.lexer) + if shouldExit, rerender, input := p.feed(b); shouldExit { + p.renderer.BreakLine(p.Buffer, p.lexer) stopReadBufCh <- struct{}{} return "" - } else if e != nil { + } else if input != nil { // Stop goroutine to run readBuffer function stopReadBufCh <- struct{}{} - return e.input - } else { - p.completion.Update(*p.buf.Document()) - p.renderer.Render(p.buf, p.completion, p.lexer) + return input.input + } else if rerender { + p.completion.Update(*p.Buffer.Document()) + p.renderer.Render(p.Buffer, p.completion, p.lexer) } default: time.Sleep(10 * time.Millisecond) @@ -416,6 +519,82 @@ func (p *Prompt) setup() { p.renderer.UpdateWinSize(p.reader.GetWinSize()) } +// Move to the left on the current line. +// Returns true when the view should be rerendered. +func (p *Prompt) CursorLeft(count istrings.RuneNumber) bool { + b := p.Buffer + cols := p.renderer.UserInputColumns() + previousCursor := b.DisplayCursorPosition(cols) + + rerender := p.Buffer.CursorLeft(count, cols, p.renderer.row) || p.completionReset || len(p.completion.tmp) > 0 + if rerender { + return true + } + + newCursor := b.DisplayCursorPosition(cols) + p.renderer.previousCursor = newCursor + p.renderer.move(previousCursor, newCursor) + p.renderer.flush() + return false +} + +// Move the cursor to the right on the current line. +// Returns true when the view should be rerendered. +func (p *Prompt) CursorRight(count istrings.RuneNumber) bool { + b := p.Buffer + cols := p.renderer.UserInputColumns() + previousCursor := b.DisplayCursorPosition(cols) + + rerender := p.Buffer.CursorRight(count, cols, p.renderer.row) || p.completionReset || len(p.completion.tmp) > 0 + if rerender { + return true + } + + newCursor := b.DisplayCursorPosition(cols) + p.renderer.previousCursor = newCursor + p.renderer.move(previousCursor, newCursor) + p.renderer.flush() + return false +} + +// Move the cursor up. +// Returns true when the view should be rerendered. +func (p *Prompt) CursorUp(count int) bool { + b := p.Buffer + cols := p.renderer.UserInputColumns() + previousCursor := b.DisplayCursorPosition(cols) + + rerender := p.Buffer.CursorUp(count, cols, p.renderer.row) || p.completionReset || len(p.completion.tmp) > 0 + if rerender { + return true + } + + newCursor := b.DisplayCursorPosition(cols) + p.renderer.previousCursor = newCursor + p.renderer.move(previousCursor, newCursor) + p.renderer.flush() + return false +} + +// Move the cursor down. +// Returns true when the view should be rerendered. +func (p *Prompt) CursorDown(count int) bool { + b := p.Buffer + cols := p.renderer.UserInputColumns() + previousCursor := b.DisplayCursorPosition(cols) + + rerender := p.Buffer.CursorDown(count, cols, p.renderer.row) || p.completionReset || len(p.completion.tmp) > 0 + if rerender { + return true + } + + newCursor := b.DisplayCursorPosition(cols) + p.renderer.previousCursor = newCursor + p.renderer.move(previousCursor, newCursor) + p.renderer.flush() + return false +} + func (p *Prompt) Close() { if !p.skipClose { debug.AssertNoError(p.reader.Close()) diff --git a/reader.go b/reader.go index a4a8b5f2..160ec511 100644 --- a/reader.go +++ b/reader.go @@ -11,6 +11,11 @@ type WinSize struct { Col uint16 } +const ( + DefColCount = 80 // Default column count of the terminal + DefRowCount = 25 // Default row count of the terminal +) + // Reader is an interface to abstract input layer. type Reader interface { // Open should be called before starting reading diff --git a/reader_posix.go b/reader_posix.go index a22d3590..cb209b25 100644 --- a/reader_posix.go +++ b/reader_posix.go @@ -58,8 +58,8 @@ func (t *PosixReader) GetWinSize() *WinSize { // If this errors, we simply return the default window size as // it's our best guess. return &WinSize{ - Row: 25, - Col: 80, + Row: DefRowCount, + Col: DefColCount, } } return &WinSize{ diff --git a/renderer.go b/renderer.go index 35805489..3d440d62 100644 --- a/renderer.go +++ b/renderer.go @@ -10,13 +10,13 @@ import ( const multilinePrefixCharacter = '.' -// Renderer to render prompt information from state of Buffer. +// Takes care of the rendering process type Renderer struct { out Writer prefixCallback PrefixCallback breakLineCallback func(*Document) title string - row uint16 + row int col istrings.Width indentSize int // How many spaces constitute a single indentation level @@ -27,8 +27,6 @@ type Renderer struct { prefixBGColor Color inputTextColor Color inputBGColor Color - previewSuggestionTextColor Color - previewSuggestionBGColor Color suggestionTextColor Color suggestionBGColor Color selectedSuggestionTextColor Color @@ -54,8 +52,6 @@ func NewRenderer() *Renderer { prefixBGColor: DefaultColor, inputTextColor: DefaultColor, inputBGColor: DefaultColor, - previewSuggestionTextColor: Green, - previewSuggestionBGColor: DefaultColor, suggestionTextColor: White, suggestionBGColor: Cyan, selectedSuggestionTextColor: Black, @@ -73,7 +69,7 @@ func NewRenderer() *Renderer { func (r *Renderer) Setup() { if r.title != "" { r.out.SetTitle(r.title) - debug.AssertNoError(r.out.Flush()) + r.flush() } } @@ -88,11 +84,11 @@ func (r *Renderer) renderPrefix(prefix string) { r.out.SetColor(DefaultColor, DefaultColor, false) } -// Close to clear title and erasing. +// Close to clear title and erase. func (r *Renderer) Close() { r.out.ClearTitle() r.out.EraseDown() - debug.AssertNoError(r.out.Flush()) + r.flush() } func (r *Renderer) prepareArea(lines int) { @@ -106,19 +102,10 @@ func (r *Renderer) prepareArea(lines int) { // UpdateWinSize called when window size is changed. func (r *Renderer) UpdateWinSize(ws *WinSize) { - r.row = ws.Row + r.row = int(ws.Row) r.col = istrings.Width(ws.Col) } -func (r *Renderer) renderWindowTooSmall() { - r.out.CursorGoTo(0, 0) - r.out.EraseScreen() - r.out.SetColor(DarkRed, White, false) - if _, err := r.out.WriteString("Your console window is too small..."); err != nil { - panic(err) - } -} - func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) { suggestions := completions.GetSuggestions() if len(suggestions) == 0 { @@ -140,7 +127,8 @@ func (r *Renderer) renderCompletion(buf *Buffer, completions *CompletionManager) formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] r.prepareArea(windowHeight) - cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), r.col-prefixWidth) + cursor := positionAtEndOfString(buf.Document().TextBeforeCursor(), r.col-prefixWidth) + cursor.X += prefixWidth x := cursor.X if x+width >= r.col { cursor = r.backward(cursor, x+width-r.col) @@ -212,65 +200,37 @@ func (r *Renderer) Render(buffer *Buffer, completion *CompletionManager, lexer L if r.col == 0 { return } - defer func() { debug.AssertNoError(r.out.Flush()) }() + defer func() { r.flush() }() r.clear(r.previousCursor) text := buffer.Text() prefix := r.prefixCallback() prefixWidth := istrings.GetWidth(prefix) - cursor := positionAtEndOfString(text, r.col-prefixWidth) + col := r.col - prefixWidth + endLine := buffer.startLine + int(r.row) - 1 + cursor := positionAtEndOfStringLine(text, col, endLine) cursor.X += prefixWidth - // prepare area - y := cursor.Y - - h := y + 1 + int(completion.max) - if h > int(r.row) || completionMargin > r.col { - r.renderWindowTooSmall() - return - } - // Rendering r.out.HideCursor() defer r.out.ShowCursor() - r.renderText(lexer, text) + r.renderText(lexer, buffer.Text(), buffer.startLine) r.out.SetColor(DefaultColor, DefaultColor, false) - targetCursor := buffer.DisplayCursorPosition(r.col - prefixWidth) + targetCursor := buffer.DisplayCursorPosition(col) targetCursor.X += prefixWidth - // Log("col: %#v, targetCursor: %#v, cursor: %#v\n", r.col-prefixWidth, targetCursor, cursor) + // Log("col: %#v, targetCursor: %#v, cursor: %#v\n", col, targetCursor, cursor) cursor = r.move(cursor, targetCursor) r.renderCompletion(buffer, completion) - if suggest, ok := completion.GetSelectedSuggestion(); ok { - cursor = r.backward(cursor, istrings.GetWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator))) - - r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false) - if _, err := r.out.WriteString(suggest.Text); err != nil { - panic(err) - } - r.out.SetColor(DefaultColor, DefaultColor, false) - cursor.X += istrings.GetWidth(suggest.Text) - endOfSuggestionPos := cursor - - rest := buffer.Document().TextAfterCursor() - - r.renderText(lexer, text) - - r.out.SetColor(DefaultColor, DefaultColor, false) - - cursor = cursor.Join(positionAtEndOfString(rest, r.col-prefixWidth)) - - cursor = r.move(cursor, endOfSuggestionPos) - } r.previousCursor = cursor } -func (r *Renderer) renderText(lexer Lexer, text string) { +func (r *Renderer) renderText(lexer Lexer, input string, startLine int) { if lexer != nil { - r.lex(lexer, text) + r.lex(lexer, input, startLine) return } @@ -278,12 +238,24 @@ func (r *Renderer) renderText(lexer Lexer, text string) { prefixWidth := istrings.GetWidth(prefix) col := r.col - prefixWidth multilinePrefix := r.getMultilinePrefix(prefix) + if startLine != 0 { + prefix = multilinePrefix + } firstIteration := true + endLine := startLine + int(r.row) var lineBuffer strings.Builder var lineCharIndex istrings.Width + var lineNumber int - for _, char := range text { + for _, char := range input { if lineCharIndex >= col || char == '\n' { + lineNumber++ + if lineNumber-1 < startLine { + continue + } + if lineNumber >= endLine { + break + } lineBuffer.WriteRune('\n') r.renderLine(prefix, lineBuffer.String(), r.inputTextColor) lineCharIndex = 0 @@ -299,6 +271,9 @@ func (r *Renderer) renderText(lexer Lexer, text string) { continue } + if lineNumber < startLine { + continue + } lineBuffer.WriteRune(char) lineCharIndex += istrings.GetRuneWidth(char) } @@ -306,18 +281,29 @@ func (r *Renderer) renderText(lexer Lexer, text string) { r.renderLine(prefix, lineBuffer.String(), r.inputTextColor) } +func (r *Renderer) flush() { + debug.AssertNoError(r.out.Flush()) +} + func (r *Renderer) renderLine(prefix, line string, color Color) { r.renderPrefix(prefix) - r.writeString(line, color) + r.writeStringColor(line, color) } -func (r *Renderer) writeString(text string, color Color) { +func (r *Renderer) writeStringColor(text string, color Color) { r.out.SetColor(color, r.inputBGColor, false) if _, err := r.out.WriteString(text); err != nil { panic(err) } } +func (r *Renderer) writeColor(b []byte, color Color) { + r.out.SetColor(color, r.inputBGColor, false) + if _, err := r.out.Write(b); err != nil { + panic(err) + } +} + func (r *Renderer) getMultilinePrefix(prefix string) string { var spaceCount int var dotCount int @@ -355,47 +341,70 @@ func (r *Renderer) getMultilinePrefix(prefix string) string { // lex processes the given input with the given lexer // and writes the result -func (r *Renderer) lex(lexer Lexer, input string) { - lexer.Init(input) - s := input - +func (r *Renderer) lex(lexer Lexer, input string, startLine int) { prefix := r.prefixCallback() prefixWidth := istrings.GetWidth(prefix) col := r.col - prefixWidth multilinePrefix := r.getMultilinePrefix(prefix) - r.renderPrefix(prefix) var lineCharIndex istrings.Width + var lineNumber int + endLine := startLine + int(r.row) + previousByteIndex := istrings.ByteNumber(-1) + lineBuffer := make([]byte, 8) + runeBuffer := make([]byte, utf8.UTFMax) + + lexer.Init(input) + + if startLine != 0 { + prefix = multilinePrefix + } + r.renderPrefix(prefix) + +tokenLoop: for { token, ok := lexer.Next() if !ok { - break + break tokenLoop } - text := strings.SplitAfter(s, token.Lexeme())[0] - s = strings.TrimPrefix(s, text) - - var lineBuffer strings.Builder + currentByteIndex := token.LastByteIndex() + text := input[previousByteIndex+1 : currentByteIndex+1] + previousByteIndex = currentByteIndex + lineBuffer = lineBuffer[:0] + charLoop: for _, char := range text { if lineCharIndex >= col || char == '\n' { - if char != '\n' { - lineBuffer.WriteByte('\n') + lineNumber++ + if lineNumber-1 < startLine { + continue charLoop + } + if lineNumber >= endLine { + break tokenLoop } - r.writeString(lineBuffer.String(), token.Color()) + lineBuffer = append(lineBuffer, '\n') + r.writeColor(lineBuffer, token.Color()) r.renderPrefix(multilinePrefix) lineCharIndex = 0 - lineBuffer.Reset() + lineBuffer = lineBuffer[:0] if char != '\n' { - lineBuffer.WriteRune(char) + size := utf8.EncodeRune(runeBuffer, char) + lineBuffer = append(lineBuffer, runeBuffer[:size]...) lineCharIndex += istrings.GetRuneWidth(char) } - continue + continue charLoop } - lineBuffer.WriteRune(char) + if lineNumber < startLine { + continue charLoop + } + size := utf8.EncodeRune(runeBuffer, char) + lineBuffer = append(lineBuffer, runeBuffer[:size]...) lineCharIndex += istrings.GetRuneWidth(char) } - r.writeString(lineBuffer.String(), token.Color()) + if len(lineBuffer) > 0 { + r.writeColor(lineBuffer, token.Color()) + } } } @@ -408,15 +417,14 @@ func (r *Renderer) BreakLine(buffer *Buffer, lexer Lexer) { cursor.X += prefixWidth r.clear(cursor) - text := buffer.Document().Text - r.renderText(lexer, text) + r.renderText(lexer, buffer.Text(), buffer.startLine) if _, err := r.out.WriteString("\n"); err != nil { panic(err) } r.out.SetColor(DefaultColor, DefaultColor, false) - debug.AssertNoError(r.out.Flush()) + r.flush() if r.breakLineCallback != nil { r.breakLineCallback(buffer.Document()) } @@ -424,6 +432,12 @@ func (r *Renderer) BreakLine(buffer *Buffer, lexer Lexer) { r.previousCursor = Position{} } +// Get the number of columns that are available +// for user input. +func (r *Renderer) UserInputColumns() istrings.Width { + return r.col - istrings.GetWidth(r.prefixCallback()) +} + // clear erases the screen from a beginning of input // even if there is line break which means input length exceeds a window's width. func (r *Renderer) clear(cursor Position) { diff --git a/writer_vt100.go b/writer_vt100.go index 7da31c21..f884cd64 100644 --- a/writer_vt100.go +++ b/writer_vt100.go @@ -224,19 +224,20 @@ func (w *VT100Writer) SetColor(fg, bg Color, bold bool) { } } +const formatSeparator = ';' + // SetDisplayAttributes to set VT100 display attributes. func (w *VT100Writer) SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribute) { w.WriteRaw([]byte{0x1b, '['}) // control sequence introducer defer w.WriteRaw([]byte{'m'}) // final character - var separator byte = ';' for i := range attrs { p, ok := displayAttributeParameters[attrs[i]] if !ok { continue } w.WriteRaw(p) - w.WriteRaw([]byte{separator}) + w.WriteRaw([]byte{formatSeparator}) } f, ok := foregroundANSIColors[fg] @@ -244,7 +245,7 @@ func (w *VT100Writer) SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribu f = foregroundANSIColors[DefaultColor] } w.WriteRaw(f) - w.WriteRaw([]byte{separator}) + w.WriteRaw([]byte{formatSeparator}) b, ok := backgroundANSIColors[bg] if !ok { b = backgroundANSIColors[DefaultColor]