mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(editor): preserve combining marks in whole-word replacement (#34957)
* fix(editor): preserve combining marks in whole-word replacement * fix(editor): keep whole-word boundaries after emoji presentation sequences Resolve a leading combining mark to its base character so VS16 after an emoji does not count as a word character, treat enclosing marks (keycaps) as symbols, and drop the redundant hook-level undo/redo test. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { findTextMatchRanges } from './markdown-preview-search'
|
||||
|
||||
describe('whole-word search around combining marks', () => {
|
||||
it.each(['́', 'ि'])('keeps the trailing combining mark %s attached to its word', (mark) => {
|
||||
const text = `cat${mark} cat ${mark}cat`
|
||||
expect(findTextMatchRanges(text, 'cat', { wholeWord: true })).toEqual([
|
||||
{ start: 4 + mark.length, end: 7 + mark.length },
|
||||
{ start: 8 + 2 * mark.length, end: 11 + 2 * mark.length }
|
||||
])
|
||||
})
|
||||
|
||||
it('does not match a Devanagari word inside a longer word ending in a vowel sign', () => {
|
||||
expect(findTextMatchRanges('किताबें किताब', 'किताब', { wholeWord: true })).toEqual([
|
||||
{ start: 8, end: 13 }
|
||||
])
|
||||
})
|
||||
|
||||
it('finds a complete decomposed accented word and preserves source code-unit offsets', () => {
|
||||
const text = 'café cafe caféine'
|
||||
expect(findTextMatchRanges(text, 'café', { wholeWord: true })).toEqual([{ start: 0, end: 5 }])
|
||||
expect(findTextMatchRanges(text, 'cafe', { wholeWord: true })).toEqual([{ start: 6, end: 10 }])
|
||||
expect(findTextMatchRanges(text, 'cafe')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['⚠️Warning here', 'Warning'],
|
||||
['✔️Done', 'Done'],
|
||||
['1️⃣Step', 'Step']
|
||||
])('matches a whole word right after an emoji presentation sequence in %s', (text, query) => {
|
||||
const start = text.indexOf(query)
|
||||
expect(findTextMatchRanges(text, query, { wholeWord: true })).toEqual([
|
||||
{ start, end: start + query.length }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -82,8 +82,10 @@ function findCaseInsensitiveMatchRanges(
|
||||
return matches
|
||||
}
|
||||
|
||||
// Treat Unicode letters/digits/underscore as word characters, matching editor whole-word find boundaries.
|
||||
const WORD_CHARACTER = /[\p{L}\p{N}_]/u
|
||||
// Combining marks stay attached to their letters when checking whole-word boundaries.
|
||||
const WORD_CHARACTER = /[\p{L}\p{M}\p{N}_]/u
|
||||
const COMBINING_MARK = /\p{M}/u
|
||||
const ENCLOSING_MARK = /\p{Me}/u
|
||||
|
||||
function isWordCharacter(char: string | undefined): boolean {
|
||||
return char !== undefined && WORD_CHARACTER.test(char)
|
||||
@@ -113,10 +115,22 @@ function codePointAt(text: string, index: number): string | undefined {
|
||||
return codePoint === undefined ? undefined : String.fromCodePoint(codePoint)
|
||||
}
|
||||
|
||||
// A leading mark belongs to the preceding base (emoji+VS16 stays a boundary); enclosing marks make a symbol (keycaps).
|
||||
function isWordGraphemeBefore(text: string, index: number): boolean {
|
||||
let offset = index
|
||||
let codePoint = codePointBefore(text, offset)
|
||||
while (codePoint !== undefined && COMBINING_MARK.test(codePoint)) {
|
||||
if (ENCLOSING_MARK.test(codePoint)) {
|
||||
return false
|
||||
}
|
||||
offset -= codePoint.length
|
||||
codePoint = codePointBefore(text, offset)
|
||||
}
|
||||
return isWordCharacter(codePoint)
|
||||
}
|
||||
|
||||
function isWholeWordMatch(text: string, start: number, end: number): boolean {
|
||||
const before = codePointBefore(text, start)
|
||||
const after = codePointAt(text, end)
|
||||
return !isWordCharacter(before) && !isWordCharacter(after)
|
||||
return !isWordGraphemeBefore(text, start) && !isWordCharacter(codePointAt(text, end))
|
||||
}
|
||||
|
||||
function buildLocaleLowercaseIndex(text: string): {
|
||||
|
||||
Reference in New Issue
Block a user