| |
| |
| |
| |
| |
|
|
| import type { TextBufferState, TextBufferAction } from './text-buffer.js'; |
| import { |
| getLineRangeOffsets, |
| getPositionFromOffsets, |
| replaceRangeInternal, |
| pushUndo, |
| detachExpandedPaste, |
| isCombiningMark, |
| findNextWordAcrossLines, |
| findPrevWordAcrossLines, |
| findNextBigWordAcrossLines, |
| findPrevBigWordAcrossLines, |
| findWordEndInLine, |
| findBigWordEndInLine, |
| } from './text-buffer.js'; |
| import { cpLen, toCodePoints } from '../../utils/textUtils.js'; |
| import { assumeExhaustive } from '@google/gemini-cli-core'; |
|
|
| export type VimAction = Extract< |
| TextBufferAction, |
| | { type: 'vim_delete_char_before' } |
| | { type: 'vim_toggle_case' } |
| | { type: 'vim_replace_char' } |
| | { type: 'vim_find_char_forward' } |
| | { type: 'vim_find_char_backward' } |
| | { type: 'vim_delete_to_char_forward' } |
| | { type: 'vim_delete_to_char_backward' } |
| | { type: 'vim_delete_word_forward' } |
| | { type: 'vim_delete_word_backward' } |
| | { type: 'vim_delete_word_end' } |
| | { type: 'vim_delete_big_word_forward' } |
| | { type: 'vim_delete_big_word_backward' } |
| | { type: 'vim_delete_big_word_end' } |
| | { type: 'vim_change_word_forward' } |
| | { type: 'vim_change_word_backward' } |
| | { type: 'vim_change_word_end' } |
| | { type: 'vim_change_big_word_forward' } |
| | { type: 'vim_change_big_word_backward' } |
| | { type: 'vim_change_big_word_end' } |
| | { type: 'vim_delete_line' } |
| | { type: 'vim_change_line' } |
| | { type: 'vim_delete_to_end_of_line' } |
| | { type: 'vim_delete_to_start_of_line' } |
| | { type: 'vim_delete_to_first_nonwhitespace' } |
| | { type: 'vim_change_to_end_of_line' } |
| | { type: 'vim_change_to_start_of_line' } |
| | { type: 'vim_change_to_first_nonwhitespace' } |
| | { type: 'vim_delete_to_first_line' } |
| | { type: 'vim_delete_to_last_line' } |
| | { type: 'vim_change_movement' } |
| | { type: 'vim_move_left' } |
| | { type: 'vim_move_right' } |
| | { type: 'vim_move_up' } |
| | { type: 'vim_move_down' } |
| | { type: 'vim_move_word_forward' } |
| | { type: 'vim_move_word_backward' } |
| | { type: 'vim_move_word_end' } |
| | { type: 'vim_move_big_word_forward' } |
| | { type: 'vim_move_big_word_backward' } |
| | { type: 'vim_move_big_word_end' } |
| | { type: 'vim_delete_char' } |
| | { type: 'vim_insert_at_cursor' } |
| | { type: 'vim_append_at_cursor' } |
| | { type: 'vim_open_line_below' } |
| | { type: 'vim_open_line_above' } |
| | { type: 'vim_append_at_line_end' } |
| | { type: 'vim_insert_at_line_start' } |
| | { type: 'vim_move_to_line_start' } |
| | { type: 'vim_move_to_line_end' } |
| | { type: 'vim_move_to_first_nonwhitespace' } |
| | { type: 'vim_move_to_first_line' } |
| | { type: 'vim_move_to_last_line' } |
| | { type: 'vim_move_to_line' } |
| | { type: 'vim_escape_insert_mode' } |
| | { type: 'vim_yank_line' } |
| | { type: 'vim_yank_word_forward' } |
| | { type: 'vim_yank_big_word_forward' } |
| | { type: 'vim_yank_word_end' } |
| | { type: 'vim_yank_big_word_end' } |
| | { type: 'vim_yank_to_end_of_line' } |
| | { type: 'vim_paste_after' } |
| | { type: 'vim_paste_before' } |
| >; |
|
|
| |
| |
| |
| |
| function findCharInLine( |
| codePoints: string[], |
| char: string, |
| count: number, |
| start: number, |
| direction: 1 | -1, |
| ): number { |
| let found = -1; |
| let hits = 0; |
| for ( |
| let i = start; |
| direction === 1 ? i < codePoints.length : i >= 0; |
| i += direction |
| ) { |
| if (codePoints[i] === char) { |
| hits++; |
| if (hits >= count) { |
| found = i; |
| break; |
| } |
| } |
| } |
| return found; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function clampNormalCursor(state: TextBufferState): TextBufferState { |
| const line = state.lines[state.cursorRow] || ''; |
| const len = cpLen(line); |
| const maxCol = Math.max(0, len - 1); |
| if (state.cursorCol <= maxCol) return state; |
| return { ...state, cursorCol: maxCol }; |
| } |
|
|
| |
| function extractRange( |
| lines: string[], |
| startRow: number, |
| startCol: number, |
| endRow: number, |
| endCol: number, |
| ): string { |
| if (startRow === endRow) { |
| return toCodePoints(lines[startRow] || '') |
| .slice(startCol, endCol) |
| .join(''); |
| } |
| const parts: string[] = []; |
| parts.push( |
| toCodePoints(lines[startRow] || '') |
| .slice(startCol) |
| .join(''), |
| ); |
| for (let r = startRow + 1; r < endRow; r++) { |
| parts.push(lines[r] || ''); |
| } |
| parts.push( |
| toCodePoints(lines[endRow] || '') |
| .slice(0, endCol) |
| .join(''), |
| ); |
| return parts.join('\n'); |
| } |
|
|
| export function handleVimAction( |
| state: TextBufferState, |
| action: VimAction, |
| ): TextBufferState { |
| const { lines, cursorRow, cursorCol } = state; |
|
|
| switch (action.type) { |
| case 'vim_delete_word_forward': |
| case 'vim_change_word_forward': { |
| const { count } = action.payload; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextWordAcrossLines(lines, endRow, endCol, true); |
| if (nextWord) { |
| endRow = nextWord.row; |
| endCol = nextWord.col; |
| } else { |
| |
| const currentLine = lines[endRow] || ''; |
| const wordEnd = findWordEndInLine(currentLine, endCol); |
|
|
| if (wordEnd !== null) { |
| |
| endCol = wordEnd + 1; |
| } |
| |
| break; |
| } |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| '', |
| ); |
| if (action.type === 'vim_delete_word_forward') { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_big_word_forward': |
| case 'vim_change_big_word_forward': { |
| const { count } = action.payload; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextBigWordAcrossLines( |
| lines, |
| endRow, |
| endCol, |
| true, |
| ); |
| if (nextWord) { |
| endRow = nextWord.row; |
| endCol = nextWord.col; |
| } else { |
| |
| const currentLine = lines[endRow] || ''; |
| const wordEnd = findBigWordEndInLine(currentLine, endCol); |
|
|
| if (wordEnd !== null) { |
| endCol = wordEnd + 1; |
| } |
| break; |
| } |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| const nextState = pushUndo(state); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| '', |
| ); |
| if (action.type === 'vim_delete_big_word_forward') { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_word_backward': |
| case 'vim_change_word_backward': { |
| const { count } = action.payload; |
| let startRow = cursorRow; |
| let startCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const prevWord = findPrevWordAcrossLines(lines, startRow, startCol); |
| if (prevWord) { |
| startRow = prevWord.row; |
| startCol = prevWord.col; |
| } else { |
| break; |
| } |
| } |
|
|
| if (startRow !== cursorRow || startCol !== cursorCol) { |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return replaceRangeInternal( |
| nextState, |
| startRow, |
| startCol, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_big_word_backward': |
| case 'vim_change_big_word_backward': { |
| const { count } = action.payload; |
| let startRow = cursorRow; |
| let startCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const prevWord = findPrevBigWordAcrossLines(lines, startRow, startCol); |
| if (prevWord) { |
| startRow = prevWord.row; |
| startCol = prevWord.col; |
| } else { |
| break; |
| } |
| } |
|
|
| if (startRow !== cursorRow || startCol !== cursorCol) { |
| const nextState = pushUndo(state); |
| return replaceRangeInternal( |
| nextState, |
| startRow, |
| startCol, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_word_end': |
| case 'vim_change_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| endRow = wordEnd.row; |
| endCol = wordEnd.col + 1; |
| |
| if (i < count - 1) { |
| const nextWord = findNextWordAcrossLines( |
| lines, |
| wordEnd.row, |
| wordEnd.col + 1, |
| true, |
| ); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| break; |
| } |
| } |
| } else { |
| break; |
| } |
| } |
|
|
| |
| if (endRow < lines.length) { |
| const lineLen = cpLen(lines[endRow] || ''); |
| endCol = Math.min(endCol, lineLen); |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| '', |
| ); |
| if (action.type === 'vim_delete_word_end') { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_big_word_end': |
| case 'vim_change_big_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextBigWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| endRow = wordEnd.row; |
| endCol = wordEnd.col + 1; |
| |
| if (i < count - 1) { |
| const nextWord = findNextBigWordAcrossLines( |
| lines, |
| wordEnd.row, |
| wordEnd.col + 1, |
| true, |
| ); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| break; |
| } |
| } |
| } else { |
| break; |
| } |
| } |
|
|
| |
| if (endRow < lines.length) { |
| const lineLen = cpLen(lines[endRow] || ''); |
| endCol = Math.min(endCol, lineLen); |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| const nextState = pushUndo(state); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| '', |
| ); |
| if (action.type === 'vim_delete_big_word_end') { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_line': { |
| const { count } = action.payload; |
| if (lines.length === 0) return state; |
|
|
| const linesToDelete = Math.min(count, lines.length - cursorRow); |
| const totalLines = lines.length; |
| const yankedText = lines |
| .slice(cursorRow, cursorRow + linesToDelete) |
| .join('\n'); |
|
|
| if (totalLines === 1 || linesToDelete >= totalLines) { |
| |
| |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...nextState, |
| lines: [''], |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| yankRegister: { text: yankedText, linewise: true }, |
| }; |
| } |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines.splice(cursorRow, linesToDelete); |
|
|
| |
| const newCursorRow = Math.min(cursorRow, newLines.length - 1); |
| const newCursorCol = 0; |
|
|
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: newCursorRow, |
| cursorCol: newCursorCol, |
| preferredCol: null, |
| yankRegister: { text: yankedText, linewise: true }, |
| }; |
| } |
|
|
| case 'vim_change_line': { |
| const { count } = action.payload; |
| if (lines.length === 0) return state; |
|
|
| const linesToChange = Math.min(count, lines.length - cursorRow); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
|
|
| const { startOffset, endOffset } = getLineRangeOffsets( |
| cursorRow, |
| linesToChange, |
| nextState.lines, |
| ); |
| const { startRow, startCol, endRow, endCol } = getPositionFromOffsets( |
| startOffset, |
| endOffset, |
| nextState.lines, |
| ); |
| return replaceRangeInternal( |
| nextState, |
| startRow, |
| startCol, |
| endRow, |
| endCol, |
| '', |
| ); |
| } |
|
|
| case 'vim_delete_to_end_of_line': |
| case 'vim_change_to_end_of_line': { |
| const { count } = action.payload; |
| const currentLine = lines[cursorRow] || ''; |
| const totalLines = lines.length; |
| const isDelete = action.type === 'vim_delete_to_end_of_line'; |
|
|
| if (count === 1) { |
| |
| if (cursorCol < cpLen(currentLine)) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cpLen(currentLine), |
| ); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cpLen(currentLine), |
| '', |
| ); |
| if (isDelete) { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } else { |
| |
| |
| const linesToDelete = Math.min(count - 1, totalLines - cursorRow - 1); |
| const endRow = cursorRow + linesToDelete; |
|
|
| if (endRow === cursorRow) { |
| |
| if (cursorCol < cpLen(currentLine)) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cpLen(currentLine), |
| ); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cpLen(currentLine), |
| '', |
| ); |
| if (isDelete) { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| return state; |
| } |
|
|
| |
| const endLine = lines[endRow] || ''; |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| cpLen(endLine), |
| ); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| endRow, |
| cpLen(endLine), |
| '', |
| ); |
| if (isDelete) { |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return newState; |
| } |
| } |
|
|
| case 'vim_delete_to_start_of_line': { |
| if (cursorCol > 0) { |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return replaceRangeInternal( |
| nextState, |
| cursorRow, |
| 0, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_to_first_nonwhitespace': { |
| |
| const currentLine = lines[cursorRow] || ''; |
| const lineCodePoints = toCodePoints(currentLine); |
| let firstNonWs = 0; |
| while ( |
| firstNonWs < lineCodePoints.length && |
| /\s/.test(lineCodePoints[firstNonWs]) |
| ) { |
| firstNonWs++; |
| } |
| |
| |
| if (firstNonWs >= lineCodePoints.length) { |
| firstNonWs = 0; |
| } |
| |
| if (cursorCol !== firstNonWs) { |
| const startCol = Math.min(cursorCol, firstNonWs); |
| const endCol = Math.max(cursorCol, firstNonWs); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return replaceRangeInternal( |
| nextState, |
| cursorRow, |
| startCol, |
| cursorRow, |
| endCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_change_to_start_of_line': { |
| |
| if (cursorCol > 0) { |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return replaceRangeInternal( |
| nextState, |
| cursorRow, |
| 0, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_change_to_first_nonwhitespace': { |
| |
| const currentLine = lines[cursorRow] || ''; |
| const lineCodePoints = toCodePoints(currentLine); |
| let firstNonWs = 0; |
| while ( |
| firstNonWs < lineCodePoints.length && |
| /\s/.test(lineCodePoints[firstNonWs]) |
| ) { |
| firstNonWs++; |
| } |
| |
| |
| if (firstNonWs >= lineCodePoints.length) { |
| firstNonWs = 0; |
| } |
| |
| if (cursorCol !== firstNonWs) { |
| const startCol = Math.min(cursorCol, firstNonWs); |
| const endCol = Math.max(cursorCol, firstNonWs); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return replaceRangeInternal( |
| nextState, |
| cursorRow, |
| startCol, |
| cursorRow, |
| endCol, |
| '', |
| ); |
| } |
| return state; |
| } |
|
|
| case 'vim_delete_to_first_line': { |
| |
| |
| const { count } = action.payload; |
| const totalLines = lines.length; |
|
|
| |
| |
| let targetRow: number; |
| if (count > 0) { |
| targetRow = Math.min(count - 1, totalLines - 1); |
| } else { |
| targetRow = 0; |
| } |
|
|
| |
| const startRow = Math.min(cursorRow, targetRow); |
| const endRow = Math.max(cursorRow, targetRow); |
| const linesToDelete = endRow - startRow + 1; |
|
|
| if (linesToDelete >= totalLines) { |
| |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...nextState, |
| lines: [''], |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines.splice(startRow, linesToDelete); |
|
|
| |
| const newCursorRow = Math.min(startRow, newLines.length - 1); |
|
|
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: newCursorRow, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_delete_to_last_line': { |
| |
| |
| const { count } = action.payload; |
| const totalLines = lines.length; |
|
|
| |
| |
| let targetRow: number; |
| if (count > 0) { |
| targetRow = Math.min(count - 1, totalLines - 1); |
| } else { |
| targetRow = totalLines - 1; |
| } |
|
|
| |
| const startRow = Math.min(cursorRow, targetRow); |
| const endRow = Math.max(cursorRow, targetRow); |
| const linesToDelete = endRow - startRow + 1; |
|
|
| if (linesToDelete >= totalLines) { |
| |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...nextState, |
| lines: [''], |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines.splice(startRow, linesToDelete); |
|
|
| |
| const newCursorRow = Math.min(startRow, newLines.length - 1); |
|
|
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: newCursorRow, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_change_movement': { |
| const { movement, count } = action.payload; |
| const totalLines = lines.length; |
|
|
| switch (movement) { |
| case 'h': { |
| |
| |
| const startCol = Math.max(0, cursorCol - count); |
| return replaceRangeInternal( |
| detachExpandedPaste(pushUndo(state)), |
| cursorRow, |
| startCol, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| } |
|
|
| case 'j': { |
| |
| const linesToChange = Math.min(count + 1, totalLines - cursorRow); |
| if (linesToChange > 0) { |
| if (linesToChange >= totalLines) { |
| |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...nextState, |
| lines: [''], |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines.splice(cursorRow, linesToChange); |
|
|
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: Math.min(cursorRow, newLines.length - 1), |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'k': { |
| |
| const startRow = Math.max(0, cursorRow - count); |
| const linesToChange = cursorRow - startRow + 1; |
|
|
| if (linesToChange > 0) { |
| if (linesToChange >= totalLines) { |
| |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...nextState, |
| lines: [''], |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines.splice(startRow, linesToChange); |
|
|
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: Math.min(startRow, newLines.length - 1), |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'l': { |
| |
| |
| return replaceRangeInternal( |
| detachExpandedPaste(pushUndo(state)), |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| Math.min(cpLen(lines[cursorRow] || ''), cursorCol + count), |
| '', |
| ); |
| } |
|
|
| default: |
| return state; |
| } |
| } |
|
|
| case 'vim_move_left': { |
| const { count } = action.payload; |
| const { cursorRow, cursorCol, lines } = state; |
| let newRow = cursorRow; |
| let newCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| if (newCol > 0) { |
| newCol--; |
| } else if (newRow > 0) { |
| |
| newRow--; |
| const prevLine = lines[newRow] || ''; |
| const prevLineLength = cpLen(prevLine); |
| |
| newCol = prevLineLength === 0 ? 0 : prevLineLength - 1; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: newRow, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_right': { |
| const { count } = action.payload; |
| const { cursorRow, cursorCol, lines } = state; |
| let newRow = cursorRow; |
| let newCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const currentLine = lines[newRow] || ''; |
| const lineLength = cpLen(currentLine); |
| |
| |
| if (lineLength === 0) { |
| |
| if (newRow < lines.length - 1) { |
| newRow++; |
| newCol = 0; |
| } |
| } else if (newCol < lineLength - 1) { |
| newCol++; |
|
|
| |
| const currentLinePoints = toCodePoints(currentLine); |
| while ( |
| newCol < currentLinePoints.length && |
| isCombiningMark(currentLinePoints[newCol]) && |
| newCol < lineLength - 1 |
| ) { |
| newCol++; |
| } |
| } else if (newRow < lines.length - 1) { |
| |
| newRow++; |
| newCol = 0; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: newRow, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_up': { |
| const { count } = action.payload; |
| const { cursorRow, cursorCol, lines } = state; |
| const newRow = Math.max(0, cursorRow - count); |
| const targetLine = lines[newRow] || ''; |
| const targetLineLength = cpLen(targetLine); |
| const newCol = Math.min( |
| cursorCol, |
| targetLineLength > 0 ? targetLineLength - 1 : 0, |
| ); |
|
|
| return { |
| ...state, |
| cursorRow: newRow, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_down': { |
| const { count } = action.payload; |
| const { cursorRow, cursorCol, lines } = state; |
| const newRow = Math.min(lines.length - 1, cursorRow + count); |
| const targetLine = lines[newRow] || ''; |
| const targetLineLength = cpLen(targetLine); |
| const newCol = Math.min( |
| cursorCol, |
| targetLineLength > 0 ? targetLineLength - 1 : 0, |
| ); |
|
|
| return { |
| ...state, |
| cursorRow: newRow, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_word_forward': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextWordAcrossLines(lines, row, col, true); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_big_word_forward': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextBigWordAcrossLines(lines, row, col, true); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_word_backward': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const prevWord = findPrevWordAcrossLines(lines, row, col); |
| if (prevWord) { |
| row = prevWord.row; |
| col = prevWord.col; |
| } else { |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_big_word_backward': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const prevWord = findPrevBigWordAcrossLines(lines, row, col); |
| if (prevWord) { |
| row = prevWord.row; |
| col = prevWord.col; |
| } else { |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| row = wordEnd.row; |
| col = wordEnd.col; |
| } else { |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_big_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextBigWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| row = wordEnd.row; |
| col = wordEnd.col; |
| } else { |
| break; |
| } |
| } |
|
|
| return { |
| ...state, |
| cursorRow: row, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_delete_char': { |
| const { count } = action.payload; |
| const { cursorRow, cursorCol, lines } = state; |
| const currentLine = lines[cursorRow] || ''; |
| const lineLength = cpLen(currentLine); |
|
|
| if (cursorCol < lineLength) { |
| const deleteCount = Math.min(count, lineLength - cursorCol); |
| const deletedText = toCodePoints(currentLine) |
| .slice(cursorCol, cursorCol + deleteCount) |
| .join(''); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cursorCol + deleteCount, |
| '', |
| ); |
| return { |
| ...clampNormalCursor(newState), |
| yankRegister: { text: deletedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_insert_at_cursor': { |
| |
| return state; |
| } |
|
|
| case 'vim_append_at_cursor': { |
| const { cursorRow, cursorCol, lines } = state; |
| const currentLine = lines[cursorRow] || ''; |
| const newCol = cursorCol < cpLen(currentLine) ? cursorCol + 1 : cursorCol; |
|
|
| return { |
| ...state, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_open_line_below': { |
| const { cursorRow, lines } = state; |
| const nextState = detachExpandedPaste(pushUndo(state)); |
|
|
| |
| const endOfLine = cpLen(lines[cursorRow] || ''); |
| return replaceRangeInternal( |
| nextState, |
| cursorRow, |
| endOfLine, |
| cursorRow, |
| endOfLine, |
| '\n', |
| ); |
| } |
|
|
| case 'vim_open_line_above': { |
| const { cursorRow } = state; |
| const nextState = detachExpandedPaste(pushUndo(state)); |
|
|
| |
| const resultState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| 0, |
| cursorRow, |
| 0, |
| '\n', |
| ); |
|
|
| |
| return { |
| ...resultState, |
| cursorRow, |
| cursorCol: 0, |
| }; |
| } |
|
|
| case 'vim_append_at_line_end': { |
| const { cursorRow, lines } = state; |
| const lineLength = cpLen(lines[cursorRow] || ''); |
|
|
| return { |
| ...state, |
| cursorCol: lineLength, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_insert_at_line_start': { |
| const { cursorRow, lines } = state; |
| const currentLine = lines[cursorRow] || ''; |
| let col = 0; |
|
|
| |
| const lineCodePoints = toCodePoints(currentLine); |
| while (col < lineCodePoints.length && /\s/.test(lineCodePoints[col])) { |
| col++; |
| } |
|
|
| return { |
| ...state, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_line_start': { |
| return { |
| ...state, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_line_end': { |
| const { cursorRow, lines } = state; |
| const lineLength = cpLen(lines[cursorRow] || ''); |
|
|
| return { |
| ...state, |
| cursorCol: lineLength > 0 ? lineLength - 1 : 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_first_nonwhitespace': { |
| const { cursorRow, lines } = state; |
| const currentLine = lines[cursorRow] || ''; |
| let col = 0; |
|
|
| |
| const lineCodePoints = toCodePoints(currentLine); |
| while (col < lineCodePoints.length && /\s/.test(lineCodePoints[col])) { |
| col++; |
| } |
|
|
| |
| if (col >= lineCodePoints.length) { |
| col = 0; |
| } |
|
|
| return { |
| ...state, |
| cursorCol: col, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_first_line': { |
| return { |
| ...state, |
| cursorRow: 0, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_last_line': { |
| const { lines } = state; |
| const lastRow = lines.length - 1; |
|
|
| return { |
| ...state, |
| cursorRow: lastRow, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_move_to_line': { |
| const { lineNumber } = action.payload; |
| const { lines } = state; |
| const targetRow = Math.min(Math.max(0, lineNumber - 1), lines.length - 1); |
|
|
| return { |
| ...state, |
| cursorRow: targetRow, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_escape_insert_mode': { |
| |
| const { cursorCol } = state; |
| const newCol = cursorCol > 0 ? cursorCol - 1 : 0; |
|
|
| return { |
| ...state, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_delete_char_before': { |
| const { count } = action.payload; |
| if (cursorCol > 0) { |
| const deleteStart = Math.max(0, cursorCol - count); |
| const deletedText = toCodePoints(lines[cursorRow] || '') |
| .slice(deleteStart, cursorCol) |
| .join(''); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| deleteStart, |
| cursorRow, |
| cursorCol, |
| '', |
| ); |
| return { |
| ...newState, |
| yankRegister: { text: deletedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_toggle_case': { |
| const { count } = action.payload; |
| const currentLine = lines[cursorRow] || ''; |
| const lineLen = cpLen(currentLine); |
| if (cursorCol >= lineLen) return state; |
| const end = Math.min(cursorCol + count, lineLen); |
| const codePoints = toCodePoints(currentLine); |
| for (let i = cursorCol; i < end; i++) { |
| const ch = codePoints[i]; |
| const upper = ch.toUpperCase(); |
| const lower = ch.toLowerCase(); |
| codePoints[i] = ch === upper ? lower : upper; |
| } |
| const newLine = codePoints.join(''); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const newLines = [...nextState.lines]; |
| newLines[cursorRow] = newLine; |
| const newCol = Math.min(end, lineLen > 0 ? lineLen - 1 : 0); |
| return { |
| ...nextState, |
| lines: newLines, |
| cursorCol: newCol, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_replace_char': { |
| const { char, count } = action.payload; |
| const currentLine = lines[cursorRow] || ''; |
| const lineLen = cpLen(currentLine); |
| if (cursorCol >= lineLen) return state; |
| const replaceCount = Math.min(count, lineLen - cursorCol); |
| const replacement = char.repeat(replaceCount); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const resultState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cursorCol + replaceCount, |
| replacement, |
| ); |
| return { |
| ...resultState, |
| cursorCol: cursorCol + replaceCount - 1, |
| preferredCol: null, |
| }; |
| } |
|
|
| case 'vim_delete_to_char_forward': { |
| const { char, count, till } = action.payload; |
| const lineCodePoints = toCodePoints(lines[cursorRow] || ''); |
| const found = findCharInLine( |
| lineCodePoints, |
| char, |
| count, |
| cursorCol + 1, |
| 1, |
| ); |
| if (found === -1) return state; |
| const endCol = till ? found : found + 1; |
| const yankedText = lineCodePoints.slice(cursorCol, endCol).join(''); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| return { |
| ...clampNormalCursor( |
| replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| endCol, |
| '', |
| ), |
| ), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
|
|
| case 'vim_delete_to_char_backward': { |
| const { char, count, till } = action.payload; |
| const lineCodePoints = toCodePoints(lines[cursorRow] || ''); |
| const found = findCharInLine( |
| lineCodePoints, |
| char, |
| count, |
| cursorCol - 1, |
| -1, |
| ); |
| if (found === -1) return state; |
| const startCol = till ? found + 1 : found; |
| const endCol = cursorCol + 1; |
| if (startCol >= endCol) return state; |
| const yankedText = lineCodePoints.slice(startCol, endCol).join(''); |
| const nextState = detachExpandedPaste(pushUndo(state)); |
| const resultState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| startCol, |
| cursorRow, |
| endCol, |
| '', |
| ); |
| return { |
| ...clampNormalCursor({ |
| ...resultState, |
| cursorCol: startCol, |
| preferredCol: null, |
| }), |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
|
|
| case 'vim_find_char_forward': { |
| const { char, count, till } = action.payload; |
| const lineCodePoints = toCodePoints(lines[cursorRow] || ''); |
| const found = findCharInLine( |
| lineCodePoints, |
| char, |
| count, |
| cursorCol + 1, |
| 1, |
| ); |
| if (found === -1) return state; |
| const newCol = till ? Math.max(cursorCol, found - 1) : found; |
| return { ...state, cursorCol: newCol, preferredCol: null }; |
| } |
|
|
| case 'vim_find_char_backward': { |
| const { char, count, till } = action.payload; |
| const lineCodePoints = toCodePoints(lines[cursorRow] || ''); |
| const found = findCharInLine( |
| lineCodePoints, |
| char, |
| count, |
| cursorCol - 1, |
| -1, |
| ); |
| if (found === -1) return state; |
| const newCol = till ? Math.min(cursorCol, found + 1) : found; |
| return { ...state, cursorCol: newCol, preferredCol: null }; |
| } |
|
|
| case 'vim_yank_line': { |
| const { count } = action.payload; |
| const linesToYank = Math.min(count, lines.length - cursorRow); |
| const text = lines.slice(cursorRow, cursorRow + linesToYank).join('\n'); |
| return { ...state, yankRegister: { text, linewise: true } }; |
| } |
|
|
| case 'vim_yank_word_forward': { |
| const { count } = action.payload; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextWordAcrossLines(lines, endRow, endCol, true); |
| if (nextWord) { |
| endRow = nextWord.row; |
| endCol = nextWord.col; |
| } else { |
| const currentLine = lines[endRow] || ''; |
| const wordEnd = findWordEndInLine(currentLine, endCol); |
| if (wordEnd !== null) { |
| endCol = wordEnd + 1; |
| } |
| break; |
| } |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| return { |
| ...state, |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_yank_big_word_forward': { |
| const { count } = action.payload; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const nextWord = findNextBigWordAcrossLines( |
| lines, |
| endRow, |
| endCol, |
| true, |
| ); |
| if (nextWord) { |
| endRow = nextWord.row; |
| endCol = nextWord.col; |
| } else { |
| const currentLine = lines[endRow] || ''; |
| const wordEnd = findBigWordEndInLine(currentLine, endCol); |
| if (wordEnd !== null) { |
| endCol = wordEnd + 1; |
| } |
| break; |
| } |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| return { |
| ...state, |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_yank_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| endRow = wordEnd.row; |
| endCol = wordEnd.col + 1; |
| if (i < count - 1) { |
| const nextWord = findNextWordAcrossLines( |
| lines, |
| wordEnd.row, |
| wordEnd.col + 1, |
| true, |
| ); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| break; |
| } |
| } |
| } else { |
| break; |
| } |
| } |
|
|
| if (endRow < lines.length) { |
| endCol = Math.min(endCol, cpLen(lines[endRow] || '')); |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| return { |
| ...state, |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_yank_big_word_end': { |
| const { count } = action.payload; |
| let row = cursorRow; |
| let col = cursorCol; |
| let endRow = cursorRow; |
| let endCol = cursorCol; |
|
|
| for (let i = 0; i < count; i++) { |
| const wordEnd = findNextBigWordAcrossLines(lines, row, col, false); |
| if (wordEnd) { |
| endRow = wordEnd.row; |
| endCol = wordEnd.col + 1; |
| if (i < count - 1) { |
| const nextWord = findNextBigWordAcrossLines( |
| lines, |
| wordEnd.row, |
| wordEnd.col + 1, |
| true, |
| ); |
| if (nextWord) { |
| row = nextWord.row; |
| col = nextWord.col; |
| } else { |
| break; |
| } |
| } |
| } else { |
| break; |
| } |
| } |
|
|
| if (endRow < lines.length) { |
| endCol = Math.min(endCol, cpLen(lines[endRow] || '')); |
| } |
|
|
| if (endRow !== cursorRow || endCol !== cursorCol) { |
| const yankedText = extractRange( |
| lines, |
| cursorRow, |
| cursorCol, |
| endRow, |
| endCol, |
| ); |
| return { |
| ...state, |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_yank_to_end_of_line': { |
| const currentLine = lines[cursorRow] || ''; |
| const lineLen = cpLen(currentLine); |
| if (cursorCol < lineLen) { |
| const yankedText = toCodePoints(currentLine).slice(cursorCol).join(''); |
| return { |
| ...state, |
| yankRegister: { text: yankedText, linewise: false }, |
| }; |
| } |
| return state; |
| } |
|
|
| case 'vim_paste_after': { |
| const { count } = action.payload; |
| const reg = state.yankRegister; |
| if (!reg) return state; |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
|
|
| if (reg.linewise) { |
| |
| const pasteText = (reg.text + '\n').repeat(count).slice(0, -1); |
| const pasteLines = pasteText.split('\n'); |
| const newLines = [...nextState.lines]; |
| newLines.splice(cursorRow + 1, 0, ...pasteLines); |
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow: cursorRow + 1, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } else { |
| |
| const currentLine = nextState.lines[cursorRow] || ''; |
| const lineLen = cpLen(currentLine); |
| const insertCol = Math.min(cursorCol + 1, lineLen); |
| const pasteText = reg.text.repeat(count); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| insertCol, |
| cursorRow, |
| insertCol, |
| pasteText, |
| ); |
| |
| |
| const pasteLength = pasteText.length; |
| return clampNormalCursor({ |
| ...newState, |
| cursorCol: Math.max( |
| 0, |
| newState.cursorCol - (pasteLength > 0 ? 1 : 0), |
| ), |
| preferredCol: null, |
| }); |
| } |
| } |
|
|
| case 'vim_paste_before': { |
| const { count } = action.payload; |
| const reg = state.yankRegister; |
| if (!reg) return state; |
|
|
| const nextState = detachExpandedPaste(pushUndo(state)); |
|
|
| if (reg.linewise) { |
| |
| const pasteText = (reg.text + '\n').repeat(count).slice(0, -1); |
| const pasteLines = pasteText.split('\n'); |
| const newLines = [...nextState.lines]; |
| newLines.splice(cursorRow, 0, ...pasteLines); |
| return { |
| ...nextState, |
| lines: newLines, |
| cursorRow, |
| cursorCol: 0, |
| preferredCol: null, |
| }; |
| } else { |
| |
| const pasteText = reg.text.repeat(count); |
| const newState = replaceRangeInternal( |
| nextState, |
| cursorRow, |
| cursorCol, |
| cursorRow, |
| cursorCol, |
| pasteText, |
| ); |
| |
| |
| const pasteLength = pasteText.length; |
| return clampNormalCursor({ |
| ...newState, |
| cursorCol: Math.max( |
| 0, |
| newState.cursorCol - (pasteLength > 0 ? 1 : 0), |
| ), |
| preferredCol: null, |
| }); |
| } |
| } |
|
|
| default: { |
| |
| assumeExhaustive(action); |
| return state; |
| } |
| } |
| } |
|
|