source dump of claude code
at main 186 lines 5.0 kB view raw
1/** 2 * Vim Text Object Finding 3 * 4 * Functions for finding text object boundaries (iw, aw, i", a(, etc.) 5 */ 6 7import { 8 isVimPunctuation, 9 isVimWhitespace, 10 isVimWordChar, 11} from '../utils/Cursor.js' 12import { getGraphemeSegmenter } from '../utils/intl.js' 13 14export type TextObjectRange = { start: number; end: number } | null 15 16/** 17 * Delimiter pairs for text objects. 18 */ 19const PAIRS: Record<string, [string, string]> = { 20 '(': ['(', ')'], 21 ')': ['(', ')'], 22 b: ['(', ')'], 23 '[': ['[', ']'], 24 ']': ['[', ']'], 25 '{': ['{', '}'], 26 '}': ['{', '}'], 27 B: ['{', '}'], 28 '<': ['<', '>'], 29 '>': ['<', '>'], 30 '"': ['"', '"'], 31 "'": ["'", "'"], 32 '`': ['`', '`'], 33} 34 35/** 36 * Find a text object at the given position. 37 */ 38export function findTextObject( 39 text: string, 40 offset: number, 41 objectType: string, 42 isInner: boolean, 43): TextObjectRange { 44 if (objectType === 'w') 45 return findWordObject(text, offset, isInner, isVimWordChar) 46 if (objectType === 'W') 47 return findWordObject(text, offset, isInner, ch => !isVimWhitespace(ch)) 48 49 const pair = PAIRS[objectType] 50 if (pair) { 51 const [open, close] = pair 52 return open === close 53 ? findQuoteObject(text, offset, open, isInner) 54 : findBracketObject(text, offset, open, close, isInner) 55 } 56 57 return null 58} 59 60function findWordObject( 61 text: string, 62 offset: number, 63 isInner: boolean, 64 isWordChar: (ch: string) => boolean, 65): TextObjectRange { 66 // Pre-segment into graphemes for grapheme-safe iteration 67 const graphemes: Array<{ segment: string; index: number }> = [] 68 for (const { segment, index } of getGraphemeSegmenter().segment(text)) { 69 graphemes.push({ segment, index }) 70 } 71 72 // Find which grapheme index the offset falls in 73 let graphemeIdx = graphemes.length - 1 74 for (let i = 0; i < graphemes.length; i++) { 75 const g = graphemes[i]! 76 const nextStart = 77 i + 1 < graphemes.length ? graphemes[i + 1]!.index : text.length 78 if (offset >= g.index && offset < nextStart) { 79 graphemeIdx = i 80 break 81 } 82 } 83 84 const graphemeAt = (idx: number): string => graphemes[idx]?.segment ?? '' 85 const offsetAt = (idx: number): number => 86 idx < graphemes.length ? graphemes[idx]!.index : text.length 87 const isWs = (idx: number): boolean => isVimWhitespace(graphemeAt(idx)) 88 const isWord = (idx: number): boolean => isWordChar(graphemeAt(idx)) 89 const isPunct = (idx: number): boolean => isVimPunctuation(graphemeAt(idx)) 90 91 let startIdx = graphemeIdx 92 let endIdx = graphemeIdx 93 94 if (isWord(graphemeIdx)) { 95 while (startIdx > 0 && isWord(startIdx - 1)) startIdx-- 96 while (endIdx < graphemes.length && isWord(endIdx)) endIdx++ 97 } else if (isWs(graphemeIdx)) { 98 while (startIdx > 0 && isWs(startIdx - 1)) startIdx-- 99 while (endIdx < graphemes.length && isWs(endIdx)) endIdx++ 100 return { start: offsetAt(startIdx), end: offsetAt(endIdx) } 101 } else if (isPunct(graphemeIdx)) { 102 while (startIdx > 0 && isPunct(startIdx - 1)) startIdx-- 103 while (endIdx < graphemes.length && isPunct(endIdx)) endIdx++ 104 } 105 106 if (!isInner) { 107 // Include surrounding whitespace 108 if (endIdx < graphemes.length && isWs(endIdx)) { 109 while (endIdx < graphemes.length && isWs(endIdx)) endIdx++ 110 } else if (startIdx > 0 && isWs(startIdx - 1)) { 111 while (startIdx > 0 && isWs(startIdx - 1)) startIdx-- 112 } 113 } 114 115 return { start: offsetAt(startIdx), end: offsetAt(endIdx) } 116} 117 118function findQuoteObject( 119 text: string, 120 offset: number, 121 quote: string, 122 isInner: boolean, 123): TextObjectRange { 124 const lineStart = text.lastIndexOf('\n', offset - 1) + 1 125 const lineEnd = text.indexOf('\n', offset) 126 const effectiveEnd = lineEnd === -1 ? text.length : lineEnd 127 const line = text.slice(lineStart, effectiveEnd) 128 const posInLine = offset - lineStart 129 130 const positions: number[] = [] 131 for (let i = 0; i < line.length; i++) { 132 if (line[i] === quote) positions.push(i) 133 } 134 135 // Pair quotes correctly: 0-1, 2-3, 4-5, etc. 136 for (let i = 0; i < positions.length - 1; i += 2) { 137 const qs = positions[i]! 138 const qe = positions[i + 1]! 139 if (qs <= posInLine && posInLine <= qe) { 140 return isInner 141 ? { start: lineStart + qs + 1, end: lineStart + qe } 142 : { start: lineStart + qs, end: lineStart + qe + 1 } 143 } 144 } 145 146 return null 147} 148 149function findBracketObject( 150 text: string, 151 offset: number, 152 open: string, 153 close: string, 154 isInner: boolean, 155): TextObjectRange { 156 let depth = 0 157 let start = -1 158 159 for (let i = offset; i >= 0; i--) { 160 if (text[i] === close && i !== offset) depth++ 161 else if (text[i] === open) { 162 if (depth === 0) { 163 start = i 164 break 165 } 166 depth-- 167 } 168 } 169 if (start === -1) return null 170 171 depth = 0 172 let end = -1 173 for (let i = start + 1; i < text.length; i++) { 174 if (text[i] === open) depth++ 175 else if (text[i] === close) { 176 if (depth === 0) { 177 end = i 178 break 179 } 180 depth-- 181 } 182 } 183 if (end === -1) return null 184 185 return isInner ? { start: start + 1, end } : { start, end: end + 1 } 186}