package prompt import ( "strings" "git.tcp.direct/tcp.direct/go-prompt/internal/debug" ) // Buffer emulates the console buffer. type Buffer struct { workingLines []string // The working lines. Similar to history workingIndex int cursorPosition int cacheDocument *Document preferredColumn int // Remember the original column for the next up/down movement. lastKeyStroke Key } // Text returns string of the current line. func (b *Buffer) Text() string { return b.workingLines[b.workingIndex] } // Document method to return document instance from the current text and cursor position. func (b *Buffer) Document() (d *Document) { if b.cacheDocument == nil || b.cacheDocument.Text != b.Text() || b.cacheDocument.cursorPosition != b.cursorPosition { b.cacheDocument = &Document{ Text: b.Text(), cursorPosition: b.cursorPosition, } } b.cacheDocument.lastKey = b.lastKeyStroke return b.cacheDocument } // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators. // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters. func (b *Buffer) DisplayCursorPosition() int { return b.Document().DisplayCursorPosition() } // InsertText insert string from current line. func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) { or := []rune(b.Text()) oc := b.cursorPosition if overwrite { overwritten := string(or[oc : oc+len(v)]) if strings.Contains(overwritten, "\n") { i := strings.IndexAny(overwritten, "\n") overwritten = overwritten[:i] } b.setText(string(or[:oc]) + v + string(or[oc+len(overwritten):])) } else { b.setText(string(or[:oc]) + v + string(or[oc:])) } if moveCursor { b.cursorPosition += len([]rune(v)) } } // 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(v string) { debug.Assert(b.cursorPosition <= len([]rune(v)), "length of input should be shorter than cursor position") b.workingLines[b.workingIndex] = v } // Set cursor position. Return whether it changed. func (b *Buffer) setCursorPosition(p int) { if p > 0 { b.cursorPosition = p } else { b.cursorPosition = 0 } } func (b *Buffer) setDocument(d *Document) { b.cacheDocument = d b.setCursorPosition(d.cursorPosition) // Call before setText because setText check the relation between cursorPosition and line length. b.setText(d.Text) } // CursorLeft move to left on the current line. func (b *Buffer) CursorLeft(count int) { l := b.Document().GetCursorLeftPosition(count) b.cursorPosition += l } // CursorRight move to right on the current line. func (b *Buffer) CursorRight(count int) { l := b.Document().GetCursorRightPosition(count) b.cursorPosition += l } // CursorUp move cursor to the previous line. // (for multi-line edit). func (b *Buffer) CursorUp(count int) { orig := b.preferredColumn if b.preferredColumn == -1 { // -1 means nil orig = b.Document().CursorPositionCol() } b.cursorPosition += b.Document().GetCursorUpPosition(count, orig) // Remember the original column for the next up/down movement. b.preferredColumn = orig } // CursorDown move cursor to the next line. // (for multi-line edit). func (b *Buffer) CursorDown(count int) { orig := b.preferredColumn if b.preferredColumn == -1 { // -1 means nil orig = b.Document().CursorPositionCol() } b.cursorPosition += b.Document().GetCursorDownPosition(count, orig) // Remember the original column for the next up/down movement. b.preferredColumn = orig } // DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text. func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) { debug.Assert(count >= 0, "count should be positive") r := []rune(b.Text()) if b.cursorPosition > 0 { start := b.cursorPosition - count if start < 0 { start = 0 } deleted = string(r[start:b.cursorPosition]) b.setDocument(&Document{ Text: string(r[:start]) + string(r[b.cursorPosition:]), cursorPosition: b.cursorPosition - len([]rune(deleted)), }) } return } // NewLine means CR. func (b *Buffer) NewLine(copyMargin bool) { if copyMargin { b.InsertText("\n"+b.Document().leadingWhitespaceInCurrentLine(), false, true) } else { b.InsertText("\n", false, true) } } // Delete specified number of characters and Return the deleted text. func (b *Buffer) Delete(count int) (deleted string) { r := []rune(b.Text()) if b.cursorPosition < len(r) { deleted = b.Document().TextAfterCursor()[:count] b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+len(deleted):])) } return } // JoinNextLine joins the next line to the current one by deleting the line ending after the current line. func (b *Buffer) JoinNextLine(separator string) { if !b.Document().OnLastLine() { b.cursorPosition += b.Document().GetEndOfLinePosition() b.Delete(1) // Remove spaces b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " ")) } } // SwapCharactersBeforeCursor swaps the last two characters before the cursor. func (b *Buffer) SwapCharactersBeforeCursor() { 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:]) } } // NewBuffer is constructor of Buffer struct. func NewBuffer() (b *Buffer) { b = &Buffer{ workingLines: []string{""}, workingIndex: 0, preferredColumn: -1, // -1 means nil } return }