document.go 13 KB

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