github.com/aretext/aretext@v1.3.0/state/cursor.go (about)

     1  package state
     2  
     3  import (
     4  	"io"
     5  
     6  	"github.com/aretext/aretext/cellwidth"
     7  	"github.com/aretext/aretext/locate"
     8  	"github.com/aretext/aretext/selection"
     9  	"github.com/aretext/aretext/text"
    10  	"github.com/aretext/aretext/text/segment"
    11  )
    12  
    13  // cursorState is the current state of the cursor.
    14  type cursorState struct {
    15  	// position is a position within the text tree where the cursor appears.
    16  	position uint64
    17  
    18  	// logicalOffset is the number of cells after the end of the line
    19  	// for the cursor's logical (not necessarily visible) position.
    20  	// This is used for navigating up/down.
    21  	// For example, consider this text, where [m] is the current cursor position.
    22  	//     1: the quick
    23  	//     2: brown
    24  	//     3: fox ju[m]ped over the lazy dog
    25  	// If the user then navigates up one line, then we'd see:
    26  	//     1: the quick
    27  	//     2: brow[n]  [*]
    28  	//     3: fox jumped over the lazy dog
    29  	// where [n] is the visible position and [*] is the logical position,
    30  	// with logicalOffset = 2.
    31  	// If the user then navigates up one line again, we'd see:
    32  	//     1: the qu[i]ck
    33  	//     2: brown
    34  	//     3: fox jumped over the lazy dog
    35  	// where [i] is the character directly above the logical position.
    36  	logicalOffset uint64
    37  }
    38  
    39  // MoveCursor moves the cursor to the specified position in the document.
    40  func MoveCursor(state *EditorState, loc Locator) {
    41  	buffer := state.documentBuffer
    42  	cursorPos := buffer.cursor.position
    43  	newPos := loc(locatorParamsForBuffer(buffer))
    44  
    45  	// Limit the position to within the document.
    46  	if n := buffer.textTree.NumChars(); newPos > n {
    47  		if n == 0 {
    48  			newPos = 0
    49  		} else {
    50  			newPos = n - 1
    51  		}
    52  	}
    53  
    54  	var logicalOffset uint64
    55  	if newPos == cursorPos {
    56  		// This handles the case where the user is moving the cursor up to a shorter line,
    57  		// then tries to move the cursor to the right at the end of the line.
    58  		// The cursor doesn't actually move, so when the user moves up another line,
    59  		// it should use the offset from the longest line.
    60  		logicalOffset = buffer.cursor.logicalOffset
    61  	}
    62  
    63  	buffer.cursor = cursorState{
    64  		position:      newPos,
    65  		logicalOffset: logicalOffset,
    66  	}
    67  }
    68  
    69  // MoveCursorToLineAbove moves the cursor up by the specified number of lines, preserving the offset within the line.
    70  func MoveCursorToLineAbove(state *EditorState, count uint64) {
    71  	buffer := state.documentBuffer
    72  	targetLineStartPos := locate.StartOfLineAbove(buffer.textTree, count, buffer.cursor.position)
    73  	moveCursorToLine(buffer, targetLineStartPos)
    74  }
    75  
    76  // MoveCursorToLineBelow moves the cursor down by the specified number of lines, preserving the offset within the line.
    77  func MoveCursorToLineBelow(state *EditorState, count uint64) {
    78  	buffer := state.documentBuffer
    79  	targetLineStartPos := locate.StartOfLineBelow(buffer.textTree, count, buffer.cursor.position)
    80  	moveCursorToLine(buffer, targetLineStartPos)
    81  }
    82  
    83  func moveCursorToLine(buffer *BufferState, targetLineStartPos uint64) {
    84  	lineStartPos := locate.StartOfLineAtPos(buffer.textTree, buffer.cursor.position)
    85  	if targetLineStartPos == lineStartPos {
    86  		return
    87  	}
    88  
    89  	targetOffset := findOffsetFromLineStart(
    90  		buffer.textTree,
    91  		lineStartPos,
    92  		buffer.cursor,
    93  		buffer.tabSize)
    94  
    95  	newPos, actualOffset := advanceToOffset(
    96  		buffer.textTree,
    97  		targetLineStartPos,
    98  		targetOffset,
    99  		buffer.tabSize)
   100  
   101  	buffer.cursor = cursorState{
   102  		position:      newPos,
   103  		logicalOffset: targetOffset - actualOffset,
   104  	}
   105  }
   106  
   107  func findOffsetFromLineStart(textTree *text.Tree, lineStartPos uint64, cursor cursorState, tabSize uint64) uint64 {
   108  	reader := textTree.ReaderAtPosition(lineStartPos)
   109  	segmentIter := segment.NewGraphemeClusterIter(reader)
   110  	seg := segment.Empty()
   111  	pos, offset := lineStartPos, uint64(0)
   112  
   113  	for {
   114  		err := segmentIter.NextSegment(seg)
   115  		if err == io.EOF || (err == nil && pos >= cursor.position) {
   116  			break
   117  		} else if err != nil {
   118  			panic(err)
   119  		}
   120  
   121  		offset += cellwidth.GraphemeClusterWidth(seg.Runes(), offset, tabSize)
   122  		pos += seg.NumRunes()
   123  	}
   124  
   125  	return offset + cursor.logicalOffset
   126  }
   127  
   128  func advanceToOffset(textTree *text.Tree, lineStartPos uint64, targetOffset uint64, tabSize uint64) (uint64, uint64) {
   129  	reader := textTree.ReaderAtPosition(lineStartPos)
   130  	segmentIter := segment.NewGraphemeClusterIter(reader)
   131  	seg := segment.Empty()
   132  	var endOfLineOrFile bool
   133  	var prevPosOffset, posOffset, cellOffset uint64
   134  
   135  	for {
   136  		err := segmentIter.NextSegment(seg)
   137  		if err == io.EOF {
   138  			endOfLineOrFile = true
   139  			break
   140  		} else if err != nil {
   141  			panic(err)
   142  		}
   143  
   144  		if seg.HasNewline() {
   145  			endOfLineOrFile = true
   146  			break
   147  		}
   148  
   149  		gcWidth := cellwidth.GraphemeClusterWidth(seg.Runes(), cellOffset, tabSize)
   150  		if cellOffset+gcWidth > targetOffset {
   151  			break
   152  		}
   153  
   154  		cellOffset += gcWidth
   155  		prevPosOffset = posOffset
   156  		posOffset += seg.NumRunes()
   157  	}
   158  
   159  	if endOfLineOrFile {
   160  		if cellOffset > 0 {
   161  			cellOffset--
   162  		}
   163  		return lineStartPos + prevPosOffset, cellOffset
   164  	}
   165  
   166  	return lineStartPos + posOffset, cellOffset
   167  }
   168  
   169  // MoveCursorToStartOfSelection moves the cursor to the start of the current selection.
   170  // If nothing is selected, this does nothing.
   171  func MoveCursorToStartOfSelection(state *EditorState) {
   172  	if state.documentBuffer.SelectionMode() == selection.ModeNone {
   173  		return
   174  	}
   175  	selectedRegion := state.documentBuffer.SelectedRegion()
   176  	MoveCursor(state, func(p LocatorParams) uint64 {
   177  		return selectedRegion.StartPos
   178  	})
   179  }
   180  
   181  // SelectRange selects a given range in charwise mode.
   182  // This will clear any prior selection and move the cursor to the end of the new selection.
   183  func SelectRange(state *EditorState, loc RangeLocator) {
   184  	startPos, endPos := loc(locatorParamsForBuffer(state.documentBuffer))
   185  	selector := state.documentBuffer.selector
   186  	selector.Clear()
   187  	selector.Start(selection.ModeChar, startPos)
   188  	MoveCursor(state, func(p LocatorParams) uint64 {
   189  		if endPos > 0 {
   190  			return endPos - 1
   191  		} else {
   192  			return 0
   193  		}
   194  	})
   195  }