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  }