forked from c-bata/go-prompt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
document.go
295 lines (260 loc) · 8.23 KB
/
document.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package prompt
import (
"sort"
"strings"
"unicode/utf8"
)
// Document has text displayed in terminal and cursor position.
type Document struct {
Text string
CursorPosition int
}
// NewDocument return the new empty document.
func NewDocument() *Document {
return &Document{
Text: "",
CursorPosition: 0,
}
}
// 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():]
}
// 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():]
}
// FindStartOfPreviousWord returns an index relative to the cursor position
// pointing to the start of the previous word. Return `None` if nothing was found.
func (d *Document) FindStartOfPreviousWord() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
x := d.TextBeforeCursor()
l := len(x)
for i := l; i > 0; i-- {
if x[i-1:i] == " " {
return i
}
}
return 0
}
// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
x := d.TextBeforeCursor()
l := len(x)
appear := false
for i := l; i > 0; i-- {
if x[i-1:i] != " " {
appear = true
}
if x[i-1:i] == " " && appear {
return i
}
}
return 0
}
// 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 = bisectRight(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
}
// bisectRight to Locate the insertion point for v in a to maintain sorted order.
func bisectRight(a []int, v int) int {
return bisectRightRange(a, v, 0, len(a))
}
func bisectRightRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] > v
})
}