/*** Vim Operator Functions** Pure functions for executing vim operators (delete, change, yank, etc.)*/import { Cursor } from '../utils/Cursor.js'import { firstGrapheme, lastGrapheme } from '../utils/intl.js'import { countCharInString } from '../utils/stringUtils.js'import {isInclusiveMotion,isLinewiseMotion,resolveMotion,} from './motions.js'import { findTextObject } from './textObjects.js'import type {FindType,Operator,RecordedChange,TextObjScope,} from './types.js'/*** Context for operator execution.*/export type OperatorContext = {cursor: Cursortext: stringsetText: (text: string) => voidsetOffset: (offset: number) => voidenterInsert: (offset: number) => voidgetRegister: () => stringsetRegister: (content: string, linewise: boolean) => voidgetLastFind: () => { type: FindType; char: string } | nullsetLastFind: (type: FindType, char: string) => voidrecordChange: (change: RecordedChange) => void}/*** Execute an operator with a simple motion.*/export function executeOperatorMotion(op: Operator,motion: string,count: number,ctx: OperatorContext,): void {const target = resolveMotion(motion, ctx.cursor, count)if (target.equals(ctx.cursor)) returnconst range = getOperatorRange(ctx.cursor, target, motion, op, count)applyOperator(op, range.from, range.to, ctx, range.linewise)ctx.recordChange({ type: 'operator', op, motion, count })}/*** Execute an operator with a find motion.*/export function executeOperatorFind(op: Operator,findType: FindType,char: string,count: number,ctx: OperatorContext,): void {const targetOffset = ctx.cursor.findCharacter(char, findType, count)if (targetOffset === null) returnconst target = new Cursor(ctx.cursor.measuredText, targetOffset)const range = getOperatorRangeForFind(ctx.cursor, target, findType)applyOperator(op, range.from, range.to, ctx)ctx.setLastFind(findType, char)ctx.recordChange({ type: 'operatorFind', op, find: findType, char, count })}/*** Execute an operator with a text object.*/export function executeOperatorTextObj(op: Operator,scope: TextObjScope,objType: string,count: number,ctx: OperatorContext,): void {const range = findTextObject(ctx.text,ctx.cursor.offset,objType,scope === 'inner',)if (!range) returnapplyOperator(op, range.start, range.end, ctx)ctx.recordChange({ type: 'operatorTextObj', op, objType, scope, count })}/*** Execute a line operation (dd, cc, yy).*/export function executeLineOp(op: Operator,count: number,ctx: OperatorContext,): void {const text = ctx.textconst lines = text.split('\n')// Calculate logical line by counting newlines before cursor offset// (cursor.getPosition() returns wrapped line which is wrong for this)const currentLine = countCharInString(text.slice(0, ctx.cursor.offset), '\n')const linesToAffect = Math.min(count, lines.length - currentLine)const lineStart = ctx.cursor.startOfLogicalLine().offsetlet lineEnd = lineStartfor (let i = 0; i < linesToAffect; i++) {const nextNewline = text.indexOf('\n', lineEnd)lineEnd = nextNewline === -1 ? text.length : nextNewline + 1}let content = text.slice(lineStart, lineEnd)// Ensure linewise content ends with newline for paste detectionif (!content.endsWith('\n')) {content = content + '\n'}ctx.setRegister(content, true)if (op === 'yank') {ctx.setOffset(lineStart)} else if (op === 'delete') {let deleteStart = lineStartconst deleteEnd = lineEnd// If deleting to end of file and there's a preceding newline, include it// This ensures deleting the last line doesn't leave a trailing newlineif (deleteEnd === text.length &&deleteStart > 0 &&text[deleteStart - 1] === '\n') {deleteStart -= 1}const newText = text.slice(0, deleteStart) + text.slice(deleteEnd)ctx.setText(newText || '')const maxOff = Math.max(0,newText.length - (lastGrapheme(newText).length || 1),)ctx.setOffset(Math.min(deleteStart, maxOff))} else if (op === 'change') {// For single line, just clear itif (lines.length === 1) {ctx.setText('')ctx.enterInsert(0)} else {// Delete all affected lines, replace with single empty line, enter insertconst beforeLines = lines.slice(0, currentLine)const afterLines = lines.slice(currentLine + linesToAffect)const newText = [...beforeLines, '', ...afterLines].join('\n')ctx.setText(newText)ctx.enterInsert(lineStart)}}ctx.recordChange({ type: 'operator', op, motion: op[0]!, count })}/*** Execute delete character (x command).*/export function executeX(count: number, ctx: OperatorContext): void {const from = ctx.cursor.offsetif (from >= ctx.text.length) return// Advance by graphemes, not code unitslet endCursor = ctx.cursorfor (let i = 0; i < count && !endCursor.isAtEnd(); i++) {endCursor = endCursor.right()}const to = endCursor.offsetconst deleted = ctx.text.slice(from, to)const newText = ctx.text.slice(0, from) + ctx.text.slice(to)ctx.setRegister(deleted, false)ctx.setText(newText)const maxOff = Math.max(0,newText.length - (lastGrapheme(newText).length || 1),)ctx.setOffset(Math.min(from, maxOff))ctx.recordChange({ type: 'x', count })}/*** Execute replace character (r command).*/export function executeReplace(char: string,count: number,ctx: OperatorContext,): void {let offset = ctx.cursor.offsetlet newText = ctx.textfor (let i = 0; i < count && offset < newText.length; i++) {const graphemeLen = firstGrapheme(newText.slice(offset)).length || 1newText =newText.slice(0, offset) + char + newText.slice(offset + graphemeLen)offset += char.length}ctx.setText(newText)ctx.setOffset(Math.max(0, offset - char.length))ctx.recordChange({ type: 'replace', char, count })}/*** Execute toggle case (~ command).*/export function executeToggleCase(count: number, ctx: OperatorContext): void {const startOffset = ctx.cursor.offsetif (startOffset >= ctx.text.length) returnlet newText = ctx.textlet offset = startOffsetlet toggled = 0while (offset < newText.length && toggled < count) {const grapheme = firstGrapheme(newText.slice(offset))const graphemeLen = grapheme.lengthconst toggledGrapheme =grapheme === grapheme.toUpperCase()? grapheme.toLowerCase(): grapheme.toUpperCase()newText =newText.slice(0, offset) +toggledGrapheme +newText.slice(offset + graphemeLen)offset += toggledGrapheme.lengthtoggled++}ctx.setText(newText)// Cursor moves to position after the last toggled character// At end of line, cursor can be at the "end" positionctx.setOffset(offset)ctx.recordChange({ type: 'toggleCase', count })}/*** Execute join lines (J command).*/export function executeJoin(count: number, ctx: OperatorContext): void {const text = ctx.textconst lines = text.split('\n')const { line: currentLine } = ctx.cursor.getPosition()if (currentLine >= lines.length - 1) returnconst linesToJoin = Math.min(count, lines.length - currentLine - 1)let joinedLine = lines[currentLine]!const cursorPos = joinedLine.lengthfor (let i = 1; i <= linesToJoin; i++) {const nextLine = (lines[currentLine + i] ?? '').trimStart()if (nextLine.length > 0) {if (!joinedLine.endsWith('') && joinedLine.length > 0) {joinedLine += ''}joinedLine += nextLine}}const newLines = [...lines.slice(0, currentLine),joinedLine,...lines.slice(currentLine + linesToJoin + 1),]const newText = newLines.join('\n')ctx.setText(newText)ctx.setOffset(getLineStartOffset(newLines, currentLine) + cursorPos)ctx.recordChange({ type: 'join', count })}/*** Execute paste (p/P command).*/export function executePaste(after: boolean,count: number,ctx: OperatorContext,): void {const register = ctx.getRegister()if (!register) returnconst isLinewise = register.endsWith('\n')const content = isLinewise ? register.slice(0, -1) : registerif (isLinewise) {const text = ctx.textconst lines = text.split('\n')const { line: currentLine } = ctx.cursor.getPosition()const insertLine = after ? currentLine + 1 : currentLineconst contentLines = content.split('\n')const repeatedLines: string[] = []for (let i = 0; i < count; i++) {repeatedLines.push(...contentLines)}const newLines = [...lines.slice(0, insertLine),...repeatedLines,...lines.slice(insertLine),]const newText = newLines.join('\n')ctx.setText(newText)ctx.setOffset(getLineStartOffset(newLines, insertLine))} else {const textToInsert = content.repeat(count)const insertPoint =after && ctx.cursor.offset < ctx.text.length? ctx.cursor.measuredText.nextOffset(ctx.cursor.offset): ctx.cursor.offsetconst newText =ctx.text.slice(0, insertPoint) +textToInsert +ctx.text.slice(insertPoint)const lastGr = lastGrapheme(textToInsert)const newOffset = insertPoint + textToInsert.length - (lastGr.length || 1)ctx.setText(newText)ctx.setOffset(Math.max(insertPoint, newOffset))}}/*** Execute indent (>> command).*/export function executeIndent(dir: '>' | '<',count: number,ctx: OperatorContext,): void {const text = ctx.textconst lines = text.split('\n')const { line: currentLine } = ctx.cursor.getPosition()const linesToAffect = Math.min(count, lines.length - currentLine)const indent = '' // Two spacesfor (let i = 0; i < linesToAffect; i++) {const lineIdx = currentLine + iconst line = lines[lineIdx] ?? ''if (dir === '>') {lines[lineIdx] = indent + line} else if (line.startsWith(indent)) {lines[lineIdx] = line.slice(indent.length)} else if (line.startsWith('\t')) {lines[lineIdx] = line.slice(1)} else {// Remove as much leading whitespace as possible up to indent lengthlet removed = 0let idx = 0while (idx < line.length &&removed < indent.length &&/\s/.test(line[idx]!)) {removed++idx++}lines[lineIdx] = line.slice(idx)}}const newText = lines.join('\n')const currentLineText = lines[currentLine] ?? ''const firstNonBlank = (currentLineText.match(/^\s*/)?.[0] ?? '').lengthctx.setText(newText)ctx.setOffset(getLineStartOffset(lines, currentLine) + firstNonBlank)ctx.recordChange({ type: 'indent', dir, count })}/*** Execute open line (o/O command).*/export function executeOpenLine(direction: 'above' | 'below',ctx: OperatorContext,): void {const text = ctx.textconst lines = text.split('\n')const { line: currentLine } = ctx.cursor.getPosition()const insertLine = direction === 'below' ? currentLine + 1 : currentLineconst newLines = [...lines.slice(0, insertLine),'',...lines.slice(insertLine),]const newText = newLines.join('\n')ctx.setText(newText)ctx.enterInsert(getLineStartOffset(newLines, insertLine))ctx.recordChange({ type: 'openLine', direction })}// ============================================================================// Internal Helpers// ============================================================================/*** Calculate the offset of a line's start position.*/function getLineStartOffset(lines: string[], lineIndex: number): number {return lines.slice(0, lineIndex).join('\n').length + (lineIndex > 0 ? 1 : 0)}function getOperatorRange(cursor: Cursor,target: Cursor,motion: string,op: Operator,count: number,): { from: number; to: number; linewise: boolean } {let from = Math.min(cursor.offset, target.offset)let to = Math.max(cursor.offset, target.offset)let linewise = false// Special case: cw/cW changes to end of word, not start of next wordif (op === 'change' && (motion === 'w' || motion === 'W')) {// For cw with count, move forward (count-1) words, then find end of that wordlet wordCursor = cursorfor (let i = 0; i < count - 1; i++) {wordCursor =motion === 'w' ? wordCursor.nextVimWord() : wordCursor.nextWORD()}const wordEnd =motion === 'w' ? wordCursor.endOfVimWord() : wordCursor.endOfWORD()to = cursor.measuredText.nextOffset(wordEnd.offset)} else if (isLinewiseMotion(motion)) {// Linewise motions extend to include entire lineslinewise = trueconst text = cursor.textconst nextNewline = text.indexOf('\n', to)if (nextNewline === -1) {// Deleting to end of file - include the preceding newline if existsto = text.lengthif (from > 0 && text[from - 1] === '\n') {from -= 1}} else {to = nextNewline + 1}} else if (isInclusiveMotion(motion) && cursor.offset <= target.offset) {to = cursor.measuredText.nextOffset(to)}// Word motions can land inside an [Image #N] chip; extend the range to// cover the whole chip so dw/cw/yw never leave a partial placeholder.from = cursor.snapOutOfImageRef(from, 'start')to = cursor.snapOutOfImageRef(to, 'end')return { from, to, linewise }}/*** Get the range for a find-based operator.* Note: _findType is unused because Cursor.findCharacter already adjusts* the offset for t/T motions. All find types are treated as inclusive here.*/function getOperatorRangeForFind(cursor: Cursor,target: Cursor,_findType: FindType,): { from: number; to: number } {const from = Math.min(cursor.offset, target.offset)const maxOffset = Math.max(cursor.offset, target.offset)const to = cursor.measuredText.nextOffset(maxOffset)return { from, to }}function applyOperator(op: Operator,from: number,to: number,ctx: OperatorContext,linewise: boolean = false,): void {let content = ctx.text.slice(from, to)// Ensure linewise content ends with newline for paste detectionif (linewise && !content.endsWith('\n')) {content = content + '\n'}ctx.setRegister(content, linewise)if (op === 'yank') {ctx.setOffset(from)} else if (op === 'delete') {const newText = ctx.text.slice(0, from) + ctx.text.slice(to)ctx.setText(newText)const maxOff = Math.max(0,newText.length - (lastGrapheme(newText).length || 1),)ctx.setOffset(Math.min(from, maxOff))} else if (op === 'change') {const newText = ctx.text.slice(0, from) + ctx.text.slice(to)ctx.setText(newText)ctx.enterInsert(from)}}export function executeOperatorG(op: Operator,count: number,ctx: OperatorContext,): void {// count=1 means no count given, target = end of file// otherwise target = line Nconst target =count === 1 ? ctx.cursor.startOfLastLine() : ctx.cursor.goToLine(count)if (target.equals(ctx.cursor)) returnconst range = getOperatorRange(ctx.cursor, target, 'G', op, count)applyOperator(op, range.from, range.to, ctx, range.linewise)ctx.recordChange({ type: 'operator', op, motion: 'G', count })}export function executeOperatorGg(op: Operator,count: number,ctx: OperatorContext,): void {// count=1 means no count given, target = first line// otherwise target = line Nconst target =count === 1 ? ctx.cursor.startOfFirstLine() : ctx.cursor.goToLine(count)if (target.equals(ctx.cursor)) returnconst range = getOperatorRange(ctx.cursor, target, 'gg', op, count)applyOperator(op, range.from, range.to, ctx, range.linewise)ctx.recordChange({ type: 'operator', op, motion: 'gg', count })}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。