...

Source file src/github.com/c-bata/go-prompt/document.go

Documentation: github.com/c-bata/go-prompt

     1  package prompt
     2  
     3  import (
     4  	"strings"
     5  	"unicode/utf8"
     6  
     7  	"github.com/c-bata/go-prompt/internal/bisect"
     8  	istrings "github.com/c-bata/go-prompt/internal/strings"
     9  	runewidth "github.com/mattn/go-runewidth"
    10  )
    11  
    12  // Document has text displayed in terminal and cursor position.
    13  type Document struct {
    14  	Text string
    15  	// This represents a index in a rune array of Document.Text.
    16  	// So if Document is "日本(cursor)語", cursorPosition is 2.
    17  	// But DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
    18  	cursorPosition int
    19  	lastKey        Key
    20  }
    21  
    22  // NewDocument return the new empty document.
    23  func NewDocument() *Document {
    24  	return &Document{
    25  		Text:           "",
    26  		cursorPosition: 0,
    27  	}
    28  }
    29  
    30  // LastKeyStroke return the last key pressed in this document.
    31  func (d *Document) LastKeyStroke() Key {
    32  	return d.lastKey
    33  }
    34  
    35  // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
    36  // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
    37  func (d *Document) DisplayCursorPosition() int {
    38  	var position int
    39  	runes := []rune(d.Text)[:d.cursorPosition]
    40  	for i := range runes {
    41  		position += runewidth.RuneWidth(runes[i])
    42  	}
    43  	return position
    44  }
    45  
    46  // GetCharRelativeToCursor return character relative to cursor position, or empty string
    47  func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
    48  	s := d.Text
    49  	cnt := 0
    50  
    51  	for len(s) > 0 {
    52  		cnt++
    53  		r, size := utf8.DecodeRuneInString(s)
    54  		if cnt == d.cursorPosition+offset {
    55  			return r
    56  		}
    57  		s = s[size:]
    58  	}
    59  	return 0
    60  }
    61  
    62  // TextBeforeCursor returns the text before the cursor.
    63  func (d *Document) TextBeforeCursor() string {
    64  	r := []rune(d.Text)
    65  	return string(r[:d.cursorPosition])
    66  }
    67  
    68  // TextAfterCursor returns the text after the cursor.
    69  func (d *Document) TextAfterCursor() string {
    70  	r := []rune(d.Text)
    71  	return string(r[d.cursorPosition:])
    72  }
    73  
    74  // GetWordBeforeCursor returns the word before the cursor.
    75  // If we have whitespace before the cursor this returns an empty string.
    76  func (d *Document) GetWordBeforeCursor() string {
    77  	x := d.TextBeforeCursor()
    78  	return x[d.FindStartOfPreviousWord():]
    79  }
    80  
    81  // GetWordAfterCursor returns the word after the cursor.
    82  // If we have whitespace after the cursor this returns an empty string.
    83  func (d *Document) GetWordAfterCursor() string {
    84  	x := d.TextAfterCursor()
    85  	return x[:d.FindEndOfCurrentWord()]
    86  }
    87  
    88  // GetWordBeforeCursorWithSpace returns the word before the cursor.
    89  // Unlike GetWordBeforeCursor, it returns string containing space
    90  func (d *Document) GetWordBeforeCursorWithSpace() string {
    91  	x := d.TextBeforeCursor()
    92  	return x[d.FindStartOfPreviousWordWithSpace():]
    93  }
    94  
    95  // GetWordAfterCursorWithSpace returns the word after the cursor.
    96  // Unlike GetWordAfterCursor, it returns string containing space
    97  func (d *Document) GetWordAfterCursorWithSpace() string {
    98  	x := d.TextAfterCursor()
    99  	return x[:d.FindEndOfCurrentWordWithSpace()]
   100  }
   101  
   102  // GetWordBeforeCursorUntilSeparator returns the text before the cursor until next separator.
   103  func (d *Document) GetWordBeforeCursorUntilSeparator(sep string) string {
   104  	x := d.TextBeforeCursor()
   105  	return x[d.FindStartOfPreviousWordUntilSeparator(sep):]
   106  }
   107  
   108  // GetWordAfterCursorUntilSeparator returns the text after the cursor until next separator.
   109  func (d *Document) GetWordAfterCursorUntilSeparator(sep string) string {
   110  	x := d.TextAfterCursor()
   111  	return x[:d.FindEndOfCurrentWordUntilSeparator(sep)]
   112  }
   113  
   114  // GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor returns the word before the cursor.
   115  // Unlike GetWordBeforeCursor, it returns string containing space
   116  func (d *Document) GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
   117  	x := d.TextBeforeCursor()
   118  	return x[d.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep):]
   119  }
   120  
   121  // GetWordAfterCursorUntilSeparatorIgnoreNextToCursor returns the word after the cursor.
   122  // Unlike GetWordAfterCursor, it returns string containing space
   123  func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
   124  	x := d.TextAfterCursor()
   125  	return x[:d.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep)]
   126  }
   127  
   128  // FindStartOfPreviousWord returns an index relative to the cursor position
   129  // pointing to the start of the previous word. Return 0 if nothing was found.
   130  func (d *Document) FindStartOfPreviousWord() int {
   131  	x := d.TextBeforeCursor()
   132  	i := strings.LastIndexByte(x, ' ')
   133  	if i != -1 {
   134  		return i + 1
   135  	}
   136  	return 0
   137  }
   138  
   139  // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
   140  // The only difference is to ignore contiguous spaces.
   141  func (d *Document) FindStartOfPreviousWordWithSpace() int {
   142  	x := d.TextBeforeCursor()
   143  	end := istrings.LastIndexNotByte(x, ' ')
   144  	if end == -1 {
   145  		return 0
   146  	}
   147  
   148  	start := strings.LastIndexByte(x[:end], ' ')
   149  	if start == -1 {
   150  		return 0
   151  	}
   152  	return start + 1
   153  }
   154  
   155  // FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord.
   156  // But this can specify Separator. Return 0 if nothing was found.
   157  func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int {
   158  	if sep == "" {
   159  		return d.FindStartOfPreviousWord()
   160  	}
   161  
   162  	x := d.TextBeforeCursor()
   163  	i := strings.LastIndexAny(x, sep)
   164  	if i != -1 {
   165  		return i + 1
   166  	}
   167  	return 0
   168  }
   169  
   170  // FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace.
   171  // But this can specify Separator. Return 0 if nothing was found.
   172  func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int {
   173  	if sep == "" {
   174  		return d.FindStartOfPreviousWordWithSpace()
   175  	}
   176  
   177  	x := d.TextBeforeCursor()
   178  	end := istrings.LastIndexNotAny(x, sep)
   179  	if end == -1 {
   180  		return 0
   181  	}
   182  	start := strings.LastIndexAny(x[:end], sep)
   183  	if start == -1 {
   184  		return 0
   185  	}
   186  	return start + 1
   187  }
   188  
   189  // FindEndOfCurrentWord returns an index relative to the cursor position.
   190  // pointing to the end of the current word. Return 0 if nothing was found.
   191  func (d *Document) FindEndOfCurrentWord() int {
   192  	x := d.TextAfterCursor()
   193  	i := strings.IndexByte(x, ' ')
   194  	if i != -1 {
   195  		return i
   196  	}
   197  	return len(x)
   198  }
   199  
   200  // FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord.
   201  // The only difference is to ignore contiguous spaces.
   202  func (d *Document) FindEndOfCurrentWordWithSpace() int {
   203  	x := d.TextAfterCursor()
   204  
   205  	start := istrings.IndexNotByte(x, ' ')
   206  	if start == -1 {
   207  		return len(x)
   208  	}
   209  
   210  	end := strings.IndexByte(x[start:], ' ')
   211  	if end == -1 {
   212  		return len(x)
   213  	}
   214  
   215  	return start + end
   216  }
   217  
   218  // FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord.
   219  // But this can specify Separator. Return 0 if nothing was found.
   220  func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int {
   221  	if sep == "" {
   222  		return d.FindEndOfCurrentWord()
   223  	}
   224  
   225  	x := d.TextAfterCursor()
   226  	i := strings.IndexAny(x, sep)
   227  	if i != -1 {
   228  		return i
   229  	}
   230  	return len(x)
   231  }
   232  
   233  // FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace.
   234  // But this can specify Separator. Return 0 if nothing was found.
   235  func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int {
   236  	if sep == "" {
   237  		return d.FindEndOfCurrentWordWithSpace()
   238  	}
   239  
   240  	x := d.TextAfterCursor()
   241  
   242  	start := istrings.IndexNotAny(x, sep)
   243  	if start == -1 {
   244  		return len(x)
   245  	}
   246  
   247  	end := strings.IndexAny(x[start:], sep)
   248  	if end == -1 {
   249  		return len(x)
   250  	}
   251  
   252  	return start + end
   253  }
   254  
   255  // CurrentLineBeforeCursor returns the text from the start of the line until the cursor.
   256  func (d *Document) CurrentLineBeforeCursor() string {
   257  	s := strings.Split(d.TextBeforeCursor(), "\n")
   258  	return s[len(s)-1]
   259  }
   260  
   261  // CurrentLineAfterCursor returns the text from the cursor until the end of the line.
   262  func (d *Document) CurrentLineAfterCursor() string {
   263  	return strings.Split(d.TextAfterCursor(), "\n")[0]
   264  }
   265  
   266  // CurrentLine return the text on the line where the cursor is. (when the input
   267  // consists of just one line, it equals `text`.
   268  func (d *Document) CurrentLine() string {
   269  	return d.CurrentLineBeforeCursor() + d.CurrentLineAfterCursor()
   270  }
   271  
   272  // Array pointing to the start indexes of all the lines.
   273  func (d *Document) lineStartIndexes() []int {
   274  	// TODO: Cache, because this is often reused.
   275  	// (If it is used, it's often used many times.
   276  	// And this has to be fast for editing big documents!)
   277  	lc := d.LineCount()
   278  	lengths := make([]int, lc)
   279  	for i, l := range d.Lines() {
   280  		lengths[i] = len(l)
   281  	}
   282  
   283  	// Calculate cumulative sums.
   284  	indexes := make([]int, lc+1)
   285  	indexes[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189
   286  	pos := 0
   287  	for i, l := range lengths {
   288  		pos += l + 1
   289  		indexes[i+1] = pos
   290  	}
   291  	if lc > 1 {
   292  		// Pop the last item. (This is not a new line.)
   293  		indexes = indexes[:lc]
   294  	}
   295  	return indexes
   296  }
   297  
   298  // For the index of a character at a certain line, calculate the index of
   299  // the first character on that line.
   300  func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) {
   301  	indexes := d.lineStartIndexes()
   302  	pos = bisect.Right(indexes, index) - 1
   303  	lineStartIndex = indexes[pos]
   304  	return
   305  }
   306  
   307  // CursorPositionRow returns the current row. (0-based.)
   308  func (d *Document) CursorPositionRow() (row int) {
   309  	row, _ = d.findLineStartIndex(d.cursorPosition)
   310  	return
   311  }
   312  
   313  // CursorPositionCol returns the current column. (0-based.)
   314  func (d *Document) CursorPositionCol() (col int) {
   315  	// Don't use self.text_before_cursor to calculate this. Creating substrings
   316  	// and splitting is too expensive for getting the cursor position.
   317  	_, index := d.findLineStartIndex(d.cursorPosition)
   318  	col = d.cursorPosition - index
   319  	return
   320  }
   321  
   322  // GetCursorLeftPosition returns the relative position for cursor left.
   323  func (d *Document) GetCursorLeftPosition(count int) int {
   324  	if count < 0 {
   325  		return d.GetCursorRightPosition(-count)
   326  	}
   327  	if d.CursorPositionCol() > count {
   328  		return -count
   329  	}
   330  	return -d.CursorPositionCol()
   331  }
   332  
   333  // GetCursorRightPosition returns relative position for cursor right.
   334  func (d *Document) GetCursorRightPosition(count int) int {
   335  	if count < 0 {
   336  		return d.GetCursorLeftPosition(-count)
   337  	}
   338  	if len(d.CurrentLineAfterCursor()) > count {
   339  		return count
   340  	}
   341  	return len(d.CurrentLineAfterCursor())
   342  }
   343  
   344  // GetCursorUpPosition return the relative cursor position (character index) where we would be
   345  // if the user pressed the arrow-up button.
   346  func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int {
   347  	var col int
   348  	if preferredColumn == -1 { // -1 means nil
   349  		col = d.CursorPositionCol()
   350  	} else {
   351  		col = preferredColumn
   352  	}
   353  
   354  	row := d.CursorPositionRow() - count
   355  	if row < 0 {
   356  		row = 0
   357  	}
   358  	return d.TranslateRowColToIndex(row, col) - d.cursorPosition
   359  }
   360  
   361  // GetCursorDownPosition return the relative cursor position (character index) where we would be if the
   362  // user pressed the arrow-down button.
   363  func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int {
   364  	var col int
   365  	if preferredColumn == -1 { // -1 means nil
   366  		col = d.CursorPositionCol()
   367  	} else {
   368  		col = preferredColumn
   369  	}
   370  	row := d.CursorPositionRow() + count
   371  	return d.TranslateRowColToIndex(row, col) - d.cursorPosition
   372  }
   373  
   374  // Lines returns the array of all the lines.
   375  func (d *Document) Lines() []string {
   376  	// TODO: Cache, because this one is reused very often.
   377  	return strings.Split(d.Text, "\n")
   378  }
   379  
   380  // LineCount return the number of lines in this document. If the document ends
   381  // with a trailing \n, that counts as the beginning of a new line.
   382  func (d *Document) LineCount() int {
   383  	return len(d.Lines())
   384  }
   385  
   386  // TranslateIndexToPosition given an index for the text, return the corresponding (row, col) tuple.
   387  // (0-based. Returns (0, 0) for index=0.)
   388  func (d *Document) TranslateIndexToPosition(index int) (row int, col int) {
   389  	row, rowIndex := d.findLineStartIndex(index)
   390  	col = index - rowIndex
   391  	return
   392  }
   393  
   394  // TranslateRowColToIndex given a (row, col), return the corresponding index.
   395  // (Row and col params are 0-based.)
   396  func (d *Document) TranslateRowColToIndex(row int, column int) (index int) {
   397  	indexes := d.lineStartIndexes()
   398  	if row < 0 {
   399  		row = 0
   400  	} else if row > len(indexes) {
   401  		row = len(indexes) - 1
   402  	}
   403  	index = indexes[row]
   404  	line := d.Lines()[row]
   405  
   406  	// python) result += max(0, min(col, len(line)))
   407  	if column > 0 || len(line) > 0 {
   408  		if column > len(line) {
   409  			index += len(line)
   410  		} else {
   411  			index += column
   412  		}
   413  	}
   414  
   415  	// Keep in range. (len(self.text) is included, because the cursor can be
   416  	// right after the end of the text as well.)
   417  	// python) result = max(0, min(result, len(self.text)))
   418  	if index > len(d.Text) {
   419  		index = len(d.Text)
   420  	}
   421  	if index < 0 {
   422  		index = 0
   423  	}
   424  	return index
   425  }
   426  
   427  // OnLastLine returns true when we are at the last line.
   428  func (d *Document) OnLastLine() bool {
   429  	return d.CursorPositionRow() == (d.LineCount() - 1)
   430  }
   431  
   432  // GetEndOfLinePosition returns relative position for the end of this line.
   433  func (d *Document) GetEndOfLinePosition() int {
   434  	return len([]rune(d.CurrentLineAfterCursor()))
   435  }
   436  
   437  func (d *Document) leadingWhitespaceInCurrentLine() (margin string) {
   438  	trimmed := strings.TrimSpace(d.CurrentLine())
   439  	margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)]
   440  	return
   441  }
   442  

View as plain text