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 }