package prompt import ( "strings" "unicode/utf8" "github.com/c-bata/go-prompt/internal/bisect" istrings "github.com/c-bata/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 }