go-prompt/document.go

442 lines
13 KiB
Go

package prompt
import (
"strings"
"unicode/utf8"
"git.tcp.direct/Mirrors/go-prompt/internal/bisect"
istrings "git.tcp.direct/Mirrors/go-prompt/internal/strings"
runewidth "github.com/mattn/go-runewidth"
)
// Document has text displayed in terminal and cursor position.
type Document struct {
Text string
// This represents a index in a rune array of Document.Text.
// So if Document is "日本(cursor)語", cursorPosition is 2.
// But DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
cursorPosition int
lastKey Key
}
// NewDocument return the new empty document.
func NewDocument() *Document {
return &Document{
Text: "",
cursorPosition: 0,
}
}
// LastKeyStroke return the last key pressed in this document.
func (d *Document) LastKeyStroke() Key {
return d.lastKey
}
// 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 (d *Document) DisplayCursorPosition() int {
var position int
runes := []rune(d.Text)[:d.cursorPosition]
for i := range runes {
position += runewidth.RuneWidth(runes[i])
}
return position
}
// GetCharRelativeToCursor return character relative to cursor position, or empty string
func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
s := d.Text
cnt := 0
for len(s) > 0 {
cnt++
r, size := utf8.DecodeRuneInString(s)
if cnt == d.cursorPosition+offset {
return r
}
s = s[size:]
}
return 0
}
// TextBeforeCursor returns the text before the cursor.
func (d *Document) TextBeforeCursor() string {
r := []rune(d.Text)
return string(r[:d.cursorPosition])
}
// TextAfterCursor returns the text after the cursor.
func (d *Document) TextAfterCursor() string {
r := []rune(d.Text)
return string(r[d.cursorPosition:])
}
// GetWordBeforeCursor returns the word before the cursor.
// If we have whitespace before the cursor this returns an empty string.
func (d *Document) GetWordBeforeCursor() string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWord():]
}
// GetWordAfterCursor returns the word after the cursor.
// If we have whitespace after the cursor this returns an empty string.
func (d *Document) GetWordAfterCursor() string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWord()]
}
// GetWordBeforeCursorWithSpace returns the word before the cursor.
// Unlike GetWordBeforeCursor, it returns string containing space
func (d *Document) GetWordBeforeCursorWithSpace() string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordWithSpace():]
}
// GetWordAfterCursorWithSpace returns the word after the cursor.
// Unlike GetWordAfterCursor, it returns string containing space
func (d *Document) GetWordAfterCursorWithSpace() string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordWithSpace()]
}
// GetWordBeforeCursorUntilSeparator returns the text before the cursor until next separator.
func (d *Document) GetWordBeforeCursorUntilSeparator(sep string) string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordUntilSeparator(sep):]
}
// GetWordAfterCursorUntilSeparator returns the text after the cursor until next separator.
func (d *Document) GetWordAfterCursorUntilSeparator(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparator(sep)]
}
// GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor returns the word before the cursor.
// Unlike GetWordBeforeCursor, it returns string containing space
func (d *Document) GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep):]
}
// GetWordAfterCursorUntilSeparatorIgnoreNextToCursor returns the word after the cursor.
// Unlike GetWordAfterCursor, it returns string containing space
func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep)]
}
// FindStartOfPreviousWord returns an index relative to the cursor position
// pointing to the start of the previous word. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWord() int {
x := d.TextBeforeCursor()
i := strings.LastIndexByte(x, ' ')
if i != -1 {
return i + 1
}
return 0
}
// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
x := d.TextBeforeCursor()
end := istrings.LastIndexNotByte(x, ' ')
if end == -1 {
return 0
}
start := strings.LastIndexByte(x[:end], ' ')
if start == -1 {
return 0
}
return start + 1
}
// FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWord()
}
x := d.TextBeforeCursor()
i := strings.LastIndexAny(x, sep)
if i != -1 {
return i + 1
}
return 0
}
// FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWordWithSpace()
}
x := d.TextBeforeCursor()
end := istrings.LastIndexNotAny(x, sep)
if end == -1 {
return 0
}
start := strings.LastIndexAny(x[:end], sep)
if start == -1 {
return 0
}
return start + 1
}
// FindEndOfCurrentWord returns an index relative to the cursor position.
// pointing to the end of the current word. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWord() int {
x := d.TextAfterCursor()
i := strings.IndexByte(x, ' ')
if i != -1 {
return i
}
return len(x)
}
// FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindEndOfCurrentWordWithSpace() int {
x := d.TextAfterCursor()
start := istrings.IndexNotByte(x, ' ')
if start == -1 {
return len(x)
}
end := strings.IndexByte(x[start:], ' ')
if end == -1 {
return len(x)
}
return start + end
}
// FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWord()
}
x := d.TextAfterCursor()
i := strings.IndexAny(x, sep)
if i != -1 {
return i
}
return len(x)
}
// FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWordWithSpace()
}
x := d.TextAfterCursor()
start := istrings.IndexNotAny(x, sep)
if start == -1 {
return len(x)
}
end := strings.IndexAny(x[start:], sep)
if end == -1 {
return len(x)
}
return start + end
}
// CurrentLineBeforeCursor returns the text from the start of the line until the cursor.
func (d *Document) CurrentLineBeforeCursor() string {
s := strings.Split(d.TextBeforeCursor(), "\n")
return s[len(s)-1]
}
// CurrentLineAfterCursor returns the text from the cursor until the end of the line.
func (d *Document) CurrentLineAfterCursor() string {
return strings.Split(d.TextAfterCursor(), "\n")[0]
}
// CurrentLine return the text on the line where the cursor is. (when the input
// consists of just one line, it equals `text`.
func (d *Document) CurrentLine() string {
return d.CurrentLineBeforeCursor() + d.CurrentLineAfterCursor()
}
// Array pointing to the start indexes of all the lines.
func (d *Document) lineStartIndexes() []int {
// TODO: Cache, because this is often reused.
// (If it is used, it's often used many times.
// And this has to be fast for editing big documents!)
lc := d.LineCount()
lengths := make([]int, lc)
for i, l := range d.Lines() {
lengths[i] = len(l)
}
// Calculate cumulative sums.
indexes := make([]int, lc+1)
indexes[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189
pos := 0
for i, l := range lengths {
pos += l + 1
indexes[i+1] = pos
}
if lc > 1 {
// Pop the last item. (This is not a new line.)
indexes = indexes[:lc]
}
return indexes
}
// For the index of a character at a certain line, calculate the index of
// the first character on that line.
func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) {
indexes := d.lineStartIndexes()
pos = bisect.Right(indexes, index) - 1
lineStartIndex = indexes[pos]
return
}
// CursorPositionRow returns the current row. (0-based.)
func (d *Document) CursorPositionRow() (row int) {
row, _ = d.findLineStartIndex(d.cursorPosition)
return
}
// CursorPositionCol returns the current column. (0-based.)
func (d *Document) CursorPositionCol() (col int) {
// Don't use self.text_before_cursor to calculate this. Creating substrings
// and splitting is too expensive for getting the cursor position.
_, index := d.findLineStartIndex(d.cursorPosition)
col = d.cursorPosition - index
return
}
// GetCursorLeftPosition returns the relative position for cursor left.
func (d *Document) GetCursorLeftPosition(count int) int {
if count < 0 {
return d.GetCursorRightPosition(-count)
}
if d.CursorPositionCol() > count {
return -count
}
return -d.CursorPositionCol()
}
// GetCursorRightPosition returns relative position for cursor right.
func (d *Document) GetCursorRightPosition(count int) int {
if count < 0 {
return d.GetCursorLeftPosition(-count)
}
if len(d.CurrentLineAfterCursor()) > count {
return count
}
return len(d.CurrentLineAfterCursor())
}
// GetCursorUpPosition return the relative cursor position (character index) where we would be
// if the user pressed the arrow-up button.
func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int {
var col int
if preferredColumn == -1 { // -1 means nil
col = d.CursorPositionCol()
} else {
col = preferredColumn
}
row := d.CursorPositionRow() - count
if row < 0 {
row = 0
}
return d.TranslateRowColToIndex(row, col) - d.cursorPosition
}
// GetCursorDownPosition return the relative cursor position (character index) where we would be if the
// user pressed the arrow-down button.
func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int {
var col int
if preferredColumn == -1 { // -1 means nil
col = d.CursorPositionCol()
} else {
col = preferredColumn
}
row := d.CursorPositionRow() + count
return d.TranslateRowColToIndex(row, col) - d.cursorPosition
}
// Lines returns the array of all the lines.
func (d *Document) Lines() []string {
// TODO: Cache, because this one is reused very often.
return strings.Split(d.Text, "\n")
}
// LineCount return the number of lines in this document. If the document ends
// with a trailing \n, that counts as the beginning of a new line.
func (d *Document) LineCount() int {
return len(d.Lines())
}
// TranslateIndexToPosition given an index for the text, return the corresponding (row, col) tuple.
// (0-based. Returns (0, 0) for index=0.)
func (d *Document) TranslateIndexToPosition(index int) (row int, col int) {
row, rowIndex := d.findLineStartIndex(index)
col = index - rowIndex
return
}
// TranslateRowColToIndex given a (row, col), return the corresponding index.
// (Row and col params are 0-based.)
func (d *Document) TranslateRowColToIndex(row int, column int) (index int) {
indexes := d.lineStartIndexes()
if row < 0 {
row = 0
} else if row > len(indexes) {
row = len(indexes) - 1
}
index = indexes[row]
line := d.Lines()[row]
// python) result += max(0, min(col, len(line)))
if column > 0 || len(line) > 0 {
if column > len(line) {
index += len(line)
} else {
index += column
}
}
// Keep in range. (len(self.text) is included, because the cursor can be
// right after the end of the text as well.)
// python) result = max(0, min(result, len(self.text)))
if index > len(d.Text) {
index = len(d.Text)
}
if index < 0 {
index = 0
}
return index
}
// OnLastLine returns true when we are at the last line.
func (d *Document) OnLastLine() bool {
return d.CursorPositionRow() == (d.LineCount() - 1)
}
// GetEndOfLinePosition returns relative position for the end of this line.
func (d *Document) GetEndOfLinePosition() int {
return len([]rune(d.CurrentLineAfterCursor()))
}
func (d *Document) leadingWhitespaceInCurrentLine() (margin string) {
trimmed := strings.TrimSpace(d.CurrentLine())
margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)]
return
}