| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- 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
- }
|