(deltaLine: number, deltaCol: number)
| 1854 | } |
| 1855 | |
| 1856 | private moveCursor(deltaLine: number, deltaCol: number): void { |
| 1857 | this.lastAction = null; |
| 1858 | const visualLines = this.buildVisualLineMap(this.lastWidth); |
| 1859 | const currentVisualLine = this.findCurrentVisualLine(visualLines); |
| 1860 | |
| 1861 | if (deltaLine !== 0) { |
| 1862 | const targetVisualLine = currentVisualLine + deltaLine; |
| 1863 | |
| 1864 | if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) { |
| 1865 | this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine); |
| 1866 | } |
| 1867 | } |
| 1868 | |
| 1869 | if (deltaCol !== 0) { |
| 1870 | const currentLine = this.state.lines[this.state.cursorLine] || ""; |
| 1871 | |
| 1872 | if (deltaCol > 0) { |
| 1873 | // Moving right - move by one grapheme (handles emojis, combining characters, etc.) |
| 1874 | if (this.state.cursorCol < currentLine.length) { |
| 1875 | const afterCursor = currentLine.slice(this.state.cursorCol); |
| 1876 | const graphemes = [...this.segment(afterCursor, "grapheme")]; |
| 1877 | const firstGrapheme = graphemes[0]; |
| 1878 | this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1)); |
| 1879 | } else if (this.state.cursorLine < this.state.lines.length - 1) { |
| 1880 | // Wrap to start of next logical line |
| 1881 | this.state.cursorLine++; |
| 1882 | this.setCursorCol(0); |
| 1883 | } else { |
| 1884 | // At end of last line - can't move, but set preferredVisualCol for up/down navigation |
| 1885 | const currentVL = visualLines[currentVisualLine]; |
| 1886 | if (currentVL) { |
| 1887 | this.preferredVisualCol = this.state.cursorCol - currentVL.startCol; |
| 1888 | } |
| 1889 | } |
| 1890 | } else { |
| 1891 | // Moving left - move by one grapheme (handles emojis, combining characters, etc.) |
| 1892 | if (this.state.cursorCol > 0) { |
| 1893 | const beforeCursor = currentLine.slice(0, this.state.cursorCol); |
| 1894 | const graphemes = [...this.segment(beforeCursor, "grapheme")]; |
| 1895 | const lastGrapheme = graphemes[graphemes.length - 1]; |
| 1896 | this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1)); |
| 1897 | } else if (this.state.cursorLine > 0) { |
| 1898 | // Wrap to end of previous logical line |
| 1899 | this.state.cursorLine--; |
| 1900 | const prevLine = this.state.lines[this.state.cursorLine] || ""; |
| 1901 | this.setCursorCol(prevLine.length); |
| 1902 | } |
| 1903 | } |
| 1904 | } |
| 1905 | |
| 1906 | // Keep an open autocomplete picker in sync with the new cursor |
| 1907 | // position: cursor movement changes the text before the cursor, so a |
| 1908 | // picker computed for the old position is stale. Re-query so it |
| 1909 | // refreshes — or closes when the new position yields no suggestions — |
| 1910 | // mirroring insertCharacter()/handleBackspace(). Without this, arrowing |
| 1911 | // left from `/cmd ` back into the command name leaves the argument |
| 1912 | // picker showing against a `/cmd` prefix (and a Tab there would |
| 1913 | // concatenate the stale suggestion onto the partial command name). |
no test coverage detected