187 lines
4.9 KiB
TypeScript
Executable File
187 lines
4.9 KiB
TypeScript
Executable File
/**
|
|
* Vim Text Object Finding
|
|
*
|
|
* Functions for finding text object boundaries (iw, aw, i", a(, etc.)
|
|
*/
|
|
|
|
import {
|
|
isVimPunctuation,
|
|
isVimWhitespace,
|
|
isVimWordChar,
|
|
} from '../utils/Cursor.js'
|
|
import { getGraphemeSegmenter } from '../utils/intl.js'
|
|
|
|
export type TextObjectRange = { start: number; end: number } | null
|
|
|
|
/**
|
|
* Delimiter pairs for text objects.
|
|
*/
|
|
const PAIRS: Record<string, [string, string]> = {
|
|
'(': ['(', ')'],
|
|
')': ['(', ')'],
|
|
b: ['(', ')'],
|
|
'[': ['[', ']'],
|
|
']': ['[', ']'],
|
|
'{': ['{', '}'],
|
|
'}': ['{', '}'],
|
|
B: ['{', '}'],
|
|
'<': ['<', '>'],
|
|
'>': ['<', '>'],
|
|
'"': ['"', '"'],
|
|
"'": ["'", "'"],
|
|
'`': ['`', '`'],
|
|
}
|
|
|
|
/**
|
|
* Find a text object at the given position.
|
|
*/
|
|
export function findTextObject(
|
|
text: string,
|
|
offset: number,
|
|
objectType: string,
|
|
isInner: boolean,
|
|
): TextObjectRange {
|
|
if (objectType === 'w')
|
|
return findWordObject(text, offset, isInner, isVimWordChar)
|
|
if (objectType === 'W')
|
|
return findWordObject(text, offset, isInner, ch => !isVimWhitespace(ch))
|
|
|
|
const pair = PAIRS[objectType]
|
|
if (pair) {
|
|
const [open, close] = pair
|
|
return open === close
|
|
? findQuoteObject(text, offset, open, isInner)
|
|
: findBracketObject(text, offset, open, close, isInner)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function findWordObject(
|
|
text: string,
|
|
offset: number,
|
|
isInner: boolean,
|
|
isWordChar: (ch: string) => boolean,
|
|
): TextObjectRange {
|
|
// Pre-segment into graphemes for grapheme-safe iteration
|
|
const graphemes: Array<{ segment: string; index: number }> = []
|
|
for (const { segment, index } of getGraphemeSegmenter().segment(text)) {
|
|
graphemes.push({ segment, index })
|
|
}
|
|
|
|
// Find which grapheme index the offset falls in
|
|
let graphemeIdx = graphemes.length - 1
|
|
for (let i = 0; i < graphemes.length; i++) {
|
|
const g = graphemes[i]!
|
|
const nextStart =
|
|
i + 1 < graphemes.length ? graphemes[i + 1]!.index : text.length
|
|
if (offset >= g.index && offset < nextStart) {
|
|
graphemeIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
const graphemeAt = (idx: number): string => graphemes[idx]?.segment ?? ''
|
|
const offsetAt = (idx: number): number =>
|
|
idx < graphemes.length ? graphemes[idx]!.index : text.length
|
|
const isWs = (idx: number): boolean => isVimWhitespace(graphemeAt(idx))
|
|
const isWord = (idx: number): boolean => isWordChar(graphemeAt(idx))
|
|
const isPunct = (idx: number): boolean => isVimPunctuation(graphemeAt(idx))
|
|
|
|
let startIdx = graphemeIdx
|
|
let endIdx = graphemeIdx
|
|
|
|
if (isWord(graphemeIdx)) {
|
|
while (startIdx > 0 && isWord(startIdx - 1)) startIdx--
|
|
while (endIdx < graphemes.length && isWord(endIdx)) endIdx++
|
|
} else if (isWs(graphemeIdx)) {
|
|
while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
|
|
while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
|
|
return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
|
|
} else if (isPunct(graphemeIdx)) {
|
|
while (startIdx > 0 && isPunct(startIdx - 1)) startIdx--
|
|
while (endIdx < graphemes.length && isPunct(endIdx)) endIdx++
|
|
}
|
|
|
|
if (!isInner) {
|
|
// Include surrounding whitespace
|
|
if (endIdx < graphemes.length && isWs(endIdx)) {
|
|
while (endIdx < graphemes.length && isWs(endIdx)) endIdx++
|
|
} else if (startIdx > 0 && isWs(startIdx - 1)) {
|
|
while (startIdx > 0 && isWs(startIdx - 1)) startIdx--
|
|
}
|
|
}
|
|
|
|
return { start: offsetAt(startIdx), end: offsetAt(endIdx) }
|
|
}
|
|
|
|
function findQuoteObject(
|
|
text: string,
|
|
offset: number,
|
|
quote: string,
|
|
isInner: boolean,
|
|
): TextObjectRange {
|
|
const lineStart = text.lastIndexOf('\n', offset - 1) + 1
|
|
const lineEnd = text.indexOf('\n', offset)
|
|
const effectiveEnd = lineEnd === -1 ? text.length : lineEnd
|
|
const line = text.slice(lineStart, effectiveEnd)
|
|
const posInLine = offset - lineStart
|
|
|
|
const positions: number[] = []
|
|
for (let i = 0; i < line.length; i++) {
|
|
if (line[i] === quote) positions.push(i)
|
|
}
|
|
|
|
// Pair quotes correctly: 0-1, 2-3, 4-5, etc.
|
|
for (let i = 0; i < positions.length - 1; i += 2) {
|
|
const qs = positions[i]!
|
|
const qe = positions[i + 1]!
|
|
if (qs <= posInLine && posInLine <= qe) {
|
|
return isInner
|
|
? { start: lineStart + qs + 1, end: lineStart + qe }
|
|
: { start: lineStart + qs, end: lineStart + qe + 1 }
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function findBracketObject(
|
|
text: string,
|
|
offset: number,
|
|
open: string,
|
|
close: string,
|
|
isInner: boolean,
|
|
): TextObjectRange {
|
|
let depth = 0
|
|
let start = -1
|
|
|
|
for (let i = offset; i >= 0; i--) {
|
|
if (text[i] === close && i !== offset) depth++
|
|
else if (text[i] === open) {
|
|
if (depth === 0) {
|
|
start = i
|
|
break
|
|
}
|
|
depth--
|
|
}
|
|
}
|
|
if (start === -1) return null
|
|
|
|
depth = 0
|
|
let end = -1
|
|
for (let i = start + 1; i < text.length; i++) {
|
|
if (text[i] === open) depth++
|
|
else if (text[i] === close) {
|
|
if (depth === 0) {
|
|
end = i
|
|
break
|
|
}
|
|
depth--
|
|
}
|
|
}
|
|
if (end === -1) return null
|
|
|
|
return isInner ? { start: start + 1, end } : { start, end: end + 1 }
|
|
}
|