github.com/aretext/aretext@v1.3.0/state/edit.go (about) 1 package state 2 3 import ( 4 "fmt" 5 "io" 6 "log" 7 "strings" 8 "unicode/utf8" 9 10 "github.com/aretext/aretext/cellwidth" 11 "github.com/aretext/aretext/clipboard" 12 "github.com/aretext/aretext/locate" 13 "github.com/aretext/aretext/selection" 14 "github.com/aretext/aretext/syntax/parser" 15 "github.com/aretext/aretext/text" 16 "github.com/aretext/aretext/text/segment" 17 "github.com/aretext/aretext/undo" 18 ) 19 20 // InsertRune inserts a single rune at the current cursor location. 21 func InsertRune(state *EditorState, r rune) { 22 InsertText(state, string(r)) 23 } 24 25 // InsertText inserts multiple runes at the current cursor location. 26 func InsertText(state *EditorState, text string) { 27 buffer := state.documentBuffer 28 startPos := buffer.cursor.position 29 if err := insertTextAtPosition(state, text, startPos, true); err != nil { 30 log.Printf("Error inserting text: %v\n", err) 31 return 32 } 33 buffer.cursor.position = startPos + uint64(utf8.RuneCountInString(text)) 34 } 35 36 // insertTextAtPosition inserts text into the document. 37 // It also updates the syntax tokens and unsaved changes flag. 38 // It does NOT move the cursor. 39 func insertTextAtPosition(state *EditorState, s string, pos uint64, updateUndoLog bool) error { 40 buffer := state.documentBuffer 41 42 var n uint64 43 for _, r := range s { 44 if err := buffer.textTree.InsertAtPosition(pos+n, r); err != nil { 45 return fmt.Errorf("text.Tree.InsertAtPosition: %w", err) 46 } 47 n++ 48 } 49 50 edit := parser.NewInsertEdit(pos, n) 51 retokenizeAfterEdit(buffer, edit) 52 53 if updateUndoLog && len(s) > 0 { 54 op := undo.InsertOp(pos, s) 55 buffer.undoLog.TrackOp(op) 56 } 57 58 return nil 59 } 60 61 func mustInsertTextAtPosition(state *EditorState, text string, pos uint64, updateUndoLog bool) { 62 err := insertTextAtPosition(state, text, pos, updateUndoLog) 63 if err != nil { 64 panic(err) 65 } 66 } 67 68 func mustInsertRuneAtPosition(state *EditorState, r rune, pos uint64, updateUndoLog bool) { 69 mustInsertTextAtPosition(state, string(r), pos, updateUndoLog) 70 } 71 72 // InsertNewline inserts a newline at the current cursor position. 73 func InsertNewline(state *EditorState) { 74 cursorPos := state.documentBuffer.cursor.position 75 mustInsertRuneAtPosition(state, '\n', cursorPos, true) 76 cursorPos++ 77 78 buffer := state.documentBuffer 79 if buffer.autoIndent { 80 deleteToNextNonWhitespace(state, cursorPos) 81 numCols := numColsIndentedPrevLine(buffer, cursorPos) 82 cursorPos = indentFromPos(state, cursorPos, numCols) 83 } 84 85 buffer.cursor = cursorState{position: cursorPos} 86 } 87 88 func deleteToNextNonWhitespace(state *EditorState, startPos uint64) { 89 pos := locate.NextNonWhitespaceOrNewline(state.documentBuffer.textTree, startPos) 90 count := pos - startPos 91 deleteRunes(state, startPos, count, true) 92 } 93 94 func numColsIndentedPrevLine(buffer *BufferState, cursorPos uint64) uint64 { 95 tabSize := buffer.tabSize 96 lineNum := buffer.textTree.LineNumForPosition(cursorPos) 97 if lineNum == 0 { 98 return 0 99 } 100 101 prevLineStartPos := buffer.textTree.LineStartPosition(lineNum - 1) 102 reader := buffer.textTree.ReaderAtPosition(prevLineStartPos) 103 iter := segment.NewGraphemeClusterIter(reader) 104 seg := segment.Empty() 105 numCols := uint64(0) 106 for { 107 err := iter.NextSegment(seg) 108 if err == io.EOF { 109 break 110 } else if err != nil { 111 panic(err) 112 } 113 114 gc := seg.Runes() 115 if gc[0] != '\t' && gc[0] != ' ' { 116 break 117 } 118 119 numCols += cellwidth.GraphemeClusterWidth(gc, numCols, tabSize) 120 } 121 122 return numCols 123 } 124 125 func indentFromPos(state *EditorState, pos uint64, numCols uint64) uint64 { 126 tabSize := state.documentBuffer.tabSize 127 tabExpand := state.documentBuffer.tabExpand 128 129 i := uint64(0) 130 for i < numCols { 131 if !tabExpand && numCols-i >= tabSize { 132 mustInsertRuneAtPosition(state, '\t', pos, true) 133 i += tabSize 134 } else { 135 mustInsertRuneAtPosition(state, ' ', pos, true) 136 i++ 137 } 138 pos++ 139 } 140 return pos 141 } 142 143 // ClearAutoIndentWhitespaceLine clears a line consisting of only whitespace characters when autoindent is enabled. 144 // This is used to remove whitespace introduced by autoindent (for example, when inserting consecutive newlines). 145 func ClearAutoIndentWhitespaceLine(state *EditorState, startOfLineLoc Locator) { 146 if !state.documentBuffer.autoIndent { 147 return 148 } 149 150 params := locatorParamsForBuffer(state.documentBuffer) 151 startOfLinePos := startOfLineLoc(params) 152 endOfLinePos := locate.NextLineBoundary(params.TextTree, true, startOfLinePos) 153 firstNonWhitespacePos := locate.NextNonWhitespaceOrNewline(params.TextTree, startOfLinePos) 154 155 if endOfLinePos > startOfLinePos && firstNonWhitespacePos == endOfLinePos { 156 numDeleted := endOfLinePos - startOfLinePos 157 deleteRunes(state, startOfLinePos, numDeleted, true) 158 MoveCursor(state, func(params LocatorParams) uint64 { 159 if params.CursorPos > endOfLinePos { 160 return params.CursorPos - numDeleted 161 } else if params.CursorPos > startOfLinePos { 162 return startOfLinePos 163 } else { 164 return params.CursorPos 165 } 166 }) 167 } 168 } 169 170 // InsertTab inserts a tab at the current cursor position. 171 func InsertTab(state *EditorState) { 172 cursorPos := state.documentBuffer.cursor.position 173 newCursorPos := insertTabsAtPos(state, cursorPos, tabText(state, 1)) 174 state.documentBuffer.cursor = cursorState{position: newCursorPos} 175 } 176 177 func tabText(state *EditorState, count uint64) string { 178 var buf []byte 179 if state.documentBuffer.tabExpand { 180 buf = make([]byte, count*state.documentBuffer.tabSize) 181 for i := 0; i < len(buf); i++ { 182 buf[i] = ' ' 183 } 184 } else { 185 buf = make([]byte, count) 186 for i := 0; i < len(buf); i++ { 187 buf[i] = '\t' 188 } 189 } 190 return string(buf) 191 } 192 193 func insertTabsAtPos(state *EditorState, pos uint64, tabs string) uint64 { 194 n := uint64(len(tabs)) 195 196 if state.documentBuffer.tabExpand { 197 // Inserted tab should end at a tab stop (aligned to multiples of tabSize from start of line). 198 offset := offsetInLine(state.documentBuffer, pos) % state.documentBuffer.tabSize 199 if offset > 0 { 200 n -= offset 201 } 202 } 203 204 mustInsertTextAtPosition(state, tabs[:n], pos, true) 205 return pos + n 206 } 207 208 func offsetInLine(buffer *BufferState, startPos uint64) uint64 { 209 var offset uint64 210 textTree := buffer.textTree 211 pos := locate.StartOfLineAtPos(textTree, startPos) 212 reader := textTree.ReaderAtPosition(pos) 213 iter := segment.NewGraphemeClusterIter(reader) 214 seg := segment.Empty() 215 for pos < startPos { 216 err := iter.NextSegment(seg) 217 if err == io.EOF { 218 break 219 } else if err != nil { 220 panic(err) 221 } 222 offset += cellwidth.GraphemeClusterWidth(seg.Runes(), offset, buffer.tabSize) 223 pos += seg.NumRunes() 224 } 225 return offset 226 } 227 228 // DeleteToPos deletes characters from the cursor position up to (but not including) the position returned by the locator. 229 // It can delete either forwards or backwards from the cursor. 230 // The cursor position will be set to the start of the deleted region, 231 // which could be on a newline character or past the end of the text. 232 func DeleteToPos(state *EditorState, loc Locator, clipboardPage clipboard.PageId) { 233 buffer := state.documentBuffer 234 startPos := buffer.cursor.position 235 deleteToPos := loc(locatorParamsForBuffer(buffer)) 236 237 var deletedText string 238 if startPos < deleteToPos { 239 deletedText = deleteRunes(state, startPos, deleteToPos-startPos, true) 240 buffer.cursor = cursorState{position: startPos} 241 } else if startPos > deleteToPos { 242 deletedText = deleteRunes(state, deleteToPos, startPos-deleteToPos, true) 243 buffer.cursor = cursorState{position: deleteToPos} 244 } 245 246 if deletedText != "" { 247 state.clipboard.Set(clipboardPage, clipboard.PageContent{ 248 Text: deletedText, 249 Linewise: false, 250 }) 251 } 252 } 253 254 // DeleteRange deletes all characters in a range (for example, a word or selection). 255 // This moves the cursor to the start position of the range. 256 func DeleteRange(state *EditorState, loc RangeLocator, clipboardPage clipboard.PageId) (uint64, uint64) { 257 buffer := state.documentBuffer 258 startPos, endPos := loc(locatorParamsForBuffer(buffer)) 259 startLoc := func(LocatorParams) uint64 { return startPos } 260 endLoc := func(LocatorParams) uint64 { return endPos } 261 MoveCursor(state, startLoc) 262 DeleteToPos(state, endLoc, clipboardPage) 263 return startPos, endPos 264 } 265 266 // DeleteLines deletes lines from the cursor's current line to the line of a target cursor. 267 // It moves the cursor to the start of the line following the last deleted line. 268 func DeleteLines(state *EditorState, targetLineLoc Locator, abortIfTargetIsCurrentLine bool, replaceWithEmptyLine bool, clipboardPage clipboard.PageId) { 269 buffer := state.documentBuffer 270 currentLine := buffer.textTree.LineNumForPosition(buffer.cursor.position) 271 targetPos := targetLineLoc(locatorParamsForBuffer(buffer)) 272 targetLine := buffer.textTree.LineNumForPosition(targetPos) 273 274 if targetLine == currentLine && abortIfTargetIsCurrentLine { 275 return 276 } 277 278 if targetLine < currentLine { 279 currentLine, targetLine = targetLine, currentLine 280 } 281 282 startPos := buffer.textTree.LineStartPosition(currentLine) 283 if startPos > 0 && targetLine+1 >= buffer.textTree.NumLines() { 284 // The last line does not have a newline at the end, so delete the newline from the end of the previous line instead. 285 startPos-- 286 } 287 288 endOfLastLineToDelPos := locate.NextLineBoundary(buffer.textTree, true, buffer.textTree.LineStartPosition(targetLine)) 289 if endOfLastLineToDelPos < buffer.textTree.NumChars() { 290 endOfLastLineToDelPos++ // Add one to include the newline at the end of the line, if it exists. 291 } 292 deletedLastLine := targetLine+1 >= buffer.textTree.NumLines() 293 294 if replaceWithEmptyLine && buffer.textTree.NumChars() > 0 { 295 mustInsertRuneAtPosition(state, '\n', endOfLastLineToDelPos, true) 296 } 297 298 numToDelete := endOfLastLineToDelPos - startPos 299 deletedText := deleteRunes(state, startPos, numToDelete, true) 300 301 buffer.cursor = cursorState{position: startPos} 302 303 if deletedLastLine && replaceWithEmptyLine { 304 // Special case: if we deleted the last line and inserted a newline, we want to place the cursor *after* the newline. 305 buffer.cursor.position++ 306 } 307 308 if buffer.cursor.position >= buffer.textTree.NumChars() { 309 buffer.cursor = cursorState{ 310 position: locate.StartOfLastLine(buffer.textTree), 311 } 312 } 313 314 if len(deletedText) > 0 { 315 state.clipboard.Set(clipboardPage, clipboard.PageContent{ 316 Text: stripStartingAndTrailingNewlines(deletedText), 317 Linewise: true, 318 }) 319 } 320 } 321 322 func stripStartingAndTrailingNewlines(s string) string { 323 if len(s) > 0 && s[0] == '\n' { 324 s = s[1:] 325 } 326 327 if len(s) > 0 && s[len(s)-1] == '\n' { 328 s = s[0 : len(s)-1] 329 } 330 331 return s 332 } 333 334 // deleteRunes deletes text from the document. 335 // It also updates the syntax token and undo log. 336 // It does NOT move the cursor. 337 func deleteRunes(state *EditorState, pos uint64, count uint64, updateUndoLog bool) string { 338 deletedRunes := make([]rune, 0, count) 339 buffer := state.documentBuffer 340 for i := uint64(0); i < count; i++ { 341 didDelete, r := buffer.textTree.DeleteAtPosition(pos) 342 if didDelete { 343 deletedRunes = append(deletedRunes, r) 344 } 345 } 346 347 edit := parser.NewDeleteEdit(pos, count) 348 retokenizeAfterEdit(buffer, edit) 349 350 deletedText := string(deletedRunes) 351 if updateUndoLog && deletedText != "" { 352 op := undo.DeleteOp(pos, deletedText) 353 buffer.undoLog.TrackOp(op) 354 } 355 356 return deletedText 357 } 358 359 // ReplaceChar replaces the character under the cursor. 360 func ReplaceChar(state *EditorState, newChar rune) { 361 buffer := state.documentBuffer 362 pos := state.documentBuffer.cursor.position 363 nextCharPos := locate.NextCharInLine(buffer.textTree, 1, true, pos) 364 365 if nextCharPos == pos { 366 // No character under the cursor on the current line, so abort. 367 return 368 } 369 370 numToDelete := nextCharPos - pos 371 deleteRunes(state, pos, numToDelete, true) 372 373 switch newChar { 374 case '\n': 375 InsertNewline(state) 376 case '\t': 377 InsertTab(state) 378 MoveCursor(state, func(p LocatorParams) uint64 { 379 return locate.PrevCharInLine(p.TextTree, 1, false, p.CursorPos) 380 }) 381 default: 382 newText := string(newChar) 383 if err := insertTextAtPosition(state, newText, pos, true); err != nil { 384 // invalid UTF-8 rune; ignore it. 385 log.Printf("Error inserting text %q: %v\n", newText, err) 386 } 387 MoveCursor(state, func(p LocatorParams) uint64 { 388 return pos 389 }) 390 } 391 } 392 393 // BeginNewLineAbove starts a new line above the current line, positioning the cursor at the end of the new line. 394 func BeginNewLineAbove(state *EditorState) { 395 autoIndent := state.documentBuffer.autoIndent 396 MoveCursor(state, func(params LocatorParams) uint64 { 397 pos := locate.PrevLineBoundary(params.TextTree, params.CursorPos) 398 if autoIndent { 399 return locate.NextNonWhitespaceOrNewline(params.TextTree, pos) 400 } else { 401 return pos 402 } 403 }) 404 405 InsertNewline(state) 406 407 MoveCursor(state, func(params LocatorParams) uint64 { 408 pos := locate.StartOfLineAbove(params.TextTree, 1, params.CursorPos) 409 if autoIndent { 410 return locate.NextNonWhitespaceOrNewline(params.TextTree, pos) 411 } else { 412 return pos 413 } 414 }) 415 } 416 417 // JoinLines joins the next line with the current line. 418 // This matches vim's behavior, which has some subtle edge cases 419 // involving empty lines and indentation at the beginning of lines. 420 func JoinLines(state *EditorState) { 421 buffer := state.documentBuffer 422 cursorPos := buffer.cursor.position 423 424 nextNewlinePos, newlineLen, foundNewline := locate.NextNewline(buffer.textTree, cursorPos) 425 if !foundNewline { 426 // If we're on the last line, do nothing. 427 return 428 } 429 430 // Delete newline and any indentation at start of next line. 431 startOfNextLinePos := nextNewlinePos + newlineLen 432 endOfIndentationPos := locate.NextNonWhitespaceOrNewline(buffer.textTree, startOfNextLinePos) 433 deleteRunes(state, nextNewlinePos, endOfIndentationPos-nextNewlinePos, true) 434 435 // Replace the newline with a space and move the cursor there. 436 mustInsertRuneAtPosition(state, ' ', nextNewlinePos, true) 437 MoveCursor(state, func(LocatorParams) uint64 { return nextNewlinePos }) 438 439 // If the space is adjacent to a newline, delete it. 440 if isAdjacentToNewlineOrEof(buffer.textTree, nextNewlinePos) { 441 deleteRunes(state, nextNewlinePos, 1, true) 442 } 443 444 // Move the cursor onto the line if necessary. 445 MoveCursor(state, func(p LocatorParams) uint64 { 446 return locate.ClosestCharOnLine(p.TextTree, p.CursorPos) 447 }) 448 } 449 450 func isAdjacentToNewlineOrEof(textTree *text.Tree, pos uint64) bool { 451 seg := segment.Empty() 452 453 reader := textTree.ReaderAtPosition(pos) 454 forwardIter := segment.NewGraphemeClusterIter(reader) 455 456 // Consume the grapheme cluster on the position. 457 if err := forwardIter.NextSegment(seg); err != nil && err != io.EOF { 458 panic(err) 459 } 460 461 // Check the next grapheme cluster after the position. 462 err := forwardIter.NextSegment(seg) 463 if err == io.EOF || (err == nil && seg.HasNewline()) { 464 return true 465 } else if err != nil { 466 panic(err) 467 } 468 469 // Check the grapheme cluster before the position. 470 reverseReader := textTree.ReverseReaderAtPosition(pos) 471 backwardIter := segment.NewReverseGraphemeClusterIter(reverseReader) 472 err = backwardIter.NextSegment(seg) 473 if err == io.EOF || (err == nil && seg.HasNewline()) { 474 return true 475 } else if err != nil { 476 panic(err) 477 } 478 479 return false 480 } 481 482 // ToggleCaseAtCursor changes the character under the cursor from upper-to-lowercase or vice-versa. 483 func ToggleCaseAtCursor(state *EditorState) { 484 buffer := state.documentBuffer 485 startPos := buffer.cursor.position 486 endPos := locate.NextCharInLine(buffer.textTree, 1, true, startPos) 487 toggleCaseForRange(state, startPos, endPos) 488 MoveCursor(state, func(p LocatorParams) uint64 { 489 return locate.NextCharInLine(buffer.textTree, 1, false, p.CursorPos) 490 }) 491 } 492 493 // ToggleCaseInSelection toggles the case of all characters in the region 494 // from the cursor position to the position found by selectionEndLoc. 495 func ToggleCaseInSelection(state *EditorState, selectionEndLoc Locator) { 496 buffer := state.documentBuffer 497 cursorPos := buffer.cursor.position 498 endPos := selectionEndLoc(locatorParamsForBuffer(buffer)) 499 toggleCaseForRange(state, cursorPos, endPos) 500 } 501 502 // toggleCaseForRange changes the case of all characters in the range [startPos, endPos) 503 // It does NOT move the cursor. 504 func toggleCaseForRange(state *EditorState, startPos uint64, endPos uint64) { 505 tree := state.documentBuffer.textTree 506 newRunes := make([]rune, 0, 1) 507 reader := tree.ReaderAtPosition(startPos) 508 for pos := startPos; pos < endPos; pos++ { 509 r, _, err := reader.ReadRune() 510 if err == io.EOF { 511 break 512 } else if err != nil { 513 panic(err) // Should never happen because the document is valid UTF-8. 514 } 515 newRunes = append(newRunes, text.ToggleRuneCase(r)) 516 } 517 deleteRunes(state, startPos, uint64(len(newRunes)), true) 518 mustInsertTextAtPosition(state, string(newRunes), startPos, true) 519 } 520 521 // IndentLines indents every line from the current cursor position to the position found by targetLineLoc. 522 func IndentLines(state *EditorState, targetLineLoc Locator, count uint64) { 523 tabs := tabText(state, count) // Allocate once for all lines. 524 changeIndentationOfLines(state, targetLineLoc, func(state *EditorState, lineNum uint64) { 525 buffer := state.documentBuffer 526 startOfLinePos := locate.StartOfLineNum(buffer.textTree, lineNum) 527 endOfLinePos := locate.NextLineBoundary(buffer.textTree, true, startOfLinePos) 528 if startOfLinePos < endOfLinePos { 529 // Indent if line is non-empty. 530 insertTabsAtPos(state, startOfLinePos, tabs) 531 } 532 }) 533 } 534 535 // OutdentLines outdents every line from the current cursor position to the position found by targetLineLoc. 536 func OutdentLines(state *EditorState, targetLineLoc Locator, count uint64) { 537 changeIndentationOfLines(state, targetLineLoc, func(state *EditorState, lineNum uint64) { 538 buffer := state.documentBuffer 539 startOfLinePos := locate.StartOfLineNum(buffer.textTree, lineNum) 540 numToDelete := numRunesInIndent(buffer, startOfLinePos, count) 541 deleteRunes(state, startOfLinePos, numToDelete, true) 542 }) 543 } 544 545 func changeIndentationOfLines(state *EditorState, targetLineLoc Locator, f func(*EditorState, uint64)) { 546 buffer := state.documentBuffer 547 currentLine := buffer.textTree.LineNumForPosition(buffer.cursor.position) 548 targetPos := targetLineLoc(locatorParamsForBuffer(buffer)) 549 targetLine := buffer.textTree.LineNumForPosition(targetPos) 550 if targetLine < currentLine { 551 currentLine, targetLine = targetLine, currentLine 552 } 553 554 for lineNum := currentLine; lineNum <= targetLine; lineNum++ { 555 f(state, lineNum) 556 } 557 558 startOfFirstLinePos := locate.StartOfLineNum(buffer.textTree, currentLine) 559 newCursorPos := locate.NextNonWhitespaceOrNewline(buffer.textTree, startOfFirstLinePos) 560 buffer.cursor = cursorState{position: newCursorPos} 561 } 562 563 func numRunesInIndent(buffer *BufferState, startOfLinePos uint64, count uint64) uint64 { 564 var offset uint64 565 pos := startOfLinePos 566 endOfIndentPos := locate.NextNonWhitespaceOrNewline(buffer.textTree, startOfLinePos) 567 reader := buffer.textTree.ReaderAtPosition(pos) 568 iter := segment.NewGraphemeClusterIter(reader) 569 seg := segment.Empty() 570 for pos < endOfIndentPos && offset < (buffer.tabSize*count) { 571 err := iter.NextSegment(seg) 572 if err == io.EOF { 573 break 574 } else if err != nil { 575 panic(err) 576 } 577 offset += cellwidth.GraphemeClusterWidth(seg.Runes(), offset, buffer.tabSize) 578 pos += seg.NumRunes() 579 } 580 581 return pos - startOfLinePos 582 } 583 584 // CopyRange copies the characters in a range to the default page in the clipboard. 585 func CopyRange(state *EditorState, page clipboard.PageId, loc RangeLocator) { 586 startPos, endPos := loc(locatorParamsForBuffer(state.documentBuffer)) 587 if startPos >= endPos { 588 return 589 } 590 text := copyText(state.documentBuffer.textTree, startPos, endPos-startPos) 591 state.clipboard.Set(page, clipboard.PageContent{Text: text}) 592 } 593 594 // CopyLine copies the line under the cursor to the default page in the clipboard. 595 func CopyLine(state *EditorState, page clipboard.PageId) { 596 buffer := state.documentBuffer 597 startPos := locate.StartOfLineAtPos(buffer.textTree, buffer.cursor.position) 598 endPos := locate.NextLineBoundary(buffer.textTree, true, startPos) 599 line := copyText(buffer.textTree, startPos, endPos-startPos) 600 content := clipboard.PageContent{ 601 Text: line, 602 Linewise: true, 603 } 604 state.clipboard.Set(page, content) 605 } 606 607 // CopySelection copies the current selection to the clipboard. 608 func CopySelection(state *EditorState, page clipboard.PageId) { 609 buffer := state.documentBuffer 610 text, r := copySelectionText(buffer) 611 content := clipboard.PageContent{Text: text} 612 if buffer.selector.Mode() == selection.ModeLine { 613 content.Linewise = true 614 } 615 state.clipboard.Set(page, content) 616 617 MoveCursor(state, func(LocatorParams) uint64 { return r.StartPos }) 618 } 619 620 // copyText copies part of the document text to a string. 621 func copyText(tree *text.Tree, pos uint64, numRunes uint64) string { 622 var sb strings.Builder 623 var offset uint64 624 reader := tree.ReaderAtPosition(pos) 625 for offset < numRunes { 626 r, _, err := reader.ReadRune() 627 if err == io.EOF { 628 break 629 } else if err != nil { 630 panic(err) // should never happen because text should be valid UTF-8 631 } 632 sb.WriteRune(r) 633 offset++ 634 } 635 return sb.String() 636 } 637 638 // copySelectionText copies the currently selected text. 639 // If no text is selected, it returns an empty string. 640 func copySelectionText(buffer *BufferState) (string, selection.Region) { 641 if buffer.selector.Mode() == selection.ModeNone { 642 return "", selection.EmptyRegion 643 } 644 r := buffer.SelectedRegion() 645 text := copyText(buffer.textTree, r.StartPos, r.EndPos-r.StartPos) 646 return text, r 647 } 648 649 // PasteAfterCursor inserts the text from the clipboard after the cursor position. 650 func PasteAfterCursor(state *EditorState, page clipboard.PageId) { 651 content := state.clipboard.Get(page) 652 pos := state.documentBuffer.cursor.position 653 if content.Linewise { 654 pos = locate.NextLineBoundary(state.documentBuffer.textTree, true, pos) 655 mustInsertRuneAtPosition(state, '\n', pos, true) 656 pos++ 657 } else { 658 pos = locate.NextCharInLine(state.documentBuffer.textTree, 1, true, pos) 659 } 660 661 err := insertTextAtPosition(state, content.Text, pos, true) 662 if err != nil { 663 log.Printf("Error pasting text: %v\n", err) 664 return 665 } 666 667 if content.Linewise { 668 MoveCursor(state, func(LocatorParams) uint64 { return pos }) 669 } else { 670 MoveCursor(state, func(params LocatorParams) uint64 { 671 posAfterInsert := pos + uint64(utf8.RuneCountInString(content.Text)) 672 return locate.PrevCharInLine(params.TextTree, 1, false, posAfterInsert) 673 }) 674 } 675 } 676 677 // PasteBeforeCursor inserts the text from the clipboard before the cursor position. 678 func PasteBeforeCursor(state *EditorState, page clipboard.PageId) { 679 content := state.clipboard.Get(page) 680 pos := state.documentBuffer.cursor.position 681 if content.Linewise { 682 pos = locate.StartOfLineAtPos(state.documentBuffer.textTree, pos) 683 mustInsertRuneAtPosition(state, '\n', pos, true) 684 } 685 686 err := insertTextAtPosition(state, content.Text, pos, true) 687 if err != nil { 688 log.Printf("Error pasting text: %v\n", err) 689 return 690 } 691 692 if content.Linewise { 693 MoveCursor(state, func(LocatorParams) uint64 { return pos }) 694 } else { 695 MoveCursor(state, func(params LocatorParams) uint64 { 696 posAfterInsert := pos + uint64(utf8.RuneCountInString(content.Text)) 697 newPos := locate.PrevChar(params.TextTree, 1, posAfterInsert) 698 return locate.ClosestCharOnLine(params.TextTree, newPos) 699 }) 700 } 701 }