1 Star 1 Fork 0

Freedom/ claude-code-source-code

加入 Gitee
与超过 1400万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
operators.ts 15.59 KB
一键复制 编辑 原始数据 按行查看 历史
sanbucat 提交于 2026-03-31 17:08 +08:00 . v2.1.88 反编译源码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
/**
* 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: Cursor
text: string
setText: (text: string) => void
setOffset: (offset: number) => void
enterInsert: (offset: number) => void
getRegister: () => string
setRegister: (content: string, linewise: boolean) => void
getLastFind: () => { type: FindType; char: string } | null
setLastFind: (type: FindType, char: string) => void
recordChange: (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)) return
const 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) return
const 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) return
applyOperator(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.text
const 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().offset
let lineEnd = lineStart
for (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 detection
if (!content.endsWith('\n')) {
content = content + '\n'
}
ctx.setRegister(content, true)
if (op === 'yank') {
ctx.setOffset(lineStart)
} else if (op === 'delete') {
let deleteStart = lineStart
const 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 newline
if (
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 it
if (lines.length === 1) {
ctx.setText('')
ctx.enterInsert(0)
} else {
// Delete all affected lines, replace with single empty line, enter insert
const 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.offset
if (from >= ctx.text.length) return
// Advance by graphemes, not code units
let endCursor = ctx.cursor
for (let i = 0; i < count && !endCursor.isAtEnd(); i++) {
endCursor = endCursor.right()
}
const to = endCursor.offset
const 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.offset
let newText = ctx.text
for (let i = 0; i < count && offset < newText.length; i++) {
const graphemeLen = firstGrapheme(newText.slice(offset)).length || 1
newText =
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.offset
if (startOffset >= ctx.text.length) return
let newText = ctx.text
let offset = startOffset
let toggled = 0
while (offset < newText.length && toggled < count) {
const grapheme = firstGrapheme(newText.slice(offset))
const graphemeLen = grapheme.length
const toggledGrapheme =
grapheme === grapheme.toUpperCase()
? grapheme.toLowerCase()
: grapheme.toUpperCase()
newText =
newText.slice(0, offset) +
toggledGrapheme +
newText.slice(offset + graphemeLen)
offset += toggledGrapheme.length
toggled++
}
ctx.setText(newText)
// Cursor moves to position after the last toggled character
// At end of line, cursor can be at the "end" position
ctx.setOffset(offset)
ctx.recordChange({ type: 'toggleCase', count })
}
/**
* Execute join lines (J command).
*/
export function executeJoin(count: number, ctx: OperatorContext): void {
const text = ctx.text
const lines = text.split('\n')
const { line: currentLine } = ctx.cursor.getPosition()
if (currentLine >= lines.length - 1) return
const linesToJoin = Math.min(count, lines.length - currentLine - 1)
let joinedLine = lines[currentLine]!
const cursorPos = joinedLine.length
for (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) return
const isLinewise = register.endsWith('\n')
const content = isLinewise ? register.slice(0, -1) : register
if (isLinewise) {
const text = ctx.text
const lines = text.split('\n')
const { line: currentLine } = ctx.cursor.getPosition()
const insertLine = after ? currentLine + 1 : currentLine
const 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.offset
const 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.text
const lines = text.split('\n')
const { line: currentLine } = ctx.cursor.getPosition()
const linesToAffect = Math.min(count, lines.length - currentLine)
const indent = ' ' // Two spaces
for (let i = 0; i < linesToAffect; i++) {
const lineIdx = currentLine + i
const 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 length
let removed = 0
let idx = 0
while (
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] ?? '').length
ctx.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.text
const lines = text.split('\n')
const { line: currentLine } = ctx.cursor.getPosition()
const insertLine = direction === 'below' ? currentLine + 1 : currentLine
const 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 word
if (op === 'change' && (motion === 'w' || motion === 'W')) {
// For cw with count, move forward (count-1) words, then find end of that word
let wordCursor = cursor
for (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 lines
linewise = true
const text = cursor.text
const nextNewline = text.indexOf('\n', to)
if (nextNewline === -1) {
// Deleting to end of file - include the preceding newline if exists
to = text.length
if (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 detection
if (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 N
const target =
count === 1 ? ctx.cursor.startOfLastLine() : ctx.cursor.goToLine(count)
if (target.equals(ctx.cursor)) return
const 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 N
const target =
count === 1 ? ctx.cursor.startOfFirstLine() : ctx.cursor.goToLine(count)
if (target.equals(ctx.cursor)) return
const 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 })
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/xddcode_admin/claude-code-source-code.git
git@gitee.com:xddcode_admin/claude-code-source-code.git
xddcode_admin
claude-code-source-code
claude-code-source-code
main

搜索帮助