Skip to content

Commit e08b98b

Browse files
committed
Fix Unicode-safe revert spacing
1 parent 6ecff9d commit e08b98b

3 files changed

Lines changed: 50 additions & 212 deletions

File tree

.claude/agents/project-manager-backlog.md

Lines changed: 0 additions & 193 deletions
This file was deleted.

Sources/TextDiff/AppKit/DiffRevertActionResolver.swift

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -349,21 +349,11 @@ enum DiffRevertActionResolver {
349349
let hasTrailingWhitespace = replacement.unicodeScalars.last
350350
.map { CharacterSet.whitespacesAndNewlines.contains($0) } ?? false
351351

352-
let beforeIsWordLike: Bool
353-
if insertionLocation > 0 {
354-
let previous = updated.substring(with: NSRange(location: insertionLocation - 1, length: 1))
355-
beforeIsWordLike = isWordLike(previous)
356-
} else {
357-
beforeIsWordLike = false
358-
}
359-
360-
let afterIsWordLike: Bool
361-
if insertionLocation < updated.length {
362-
let next = updated.substring(with: NSRange(location: insertionLocation, length: 1))
363-
afterIsWordLike = isWordLike(next)
364-
} else {
365-
afterIsWordLike = false
366-
}
352+
let updatedString = updated as String
353+
let beforeIsWordLike = characterBeforeUTF16Offset(insertionLocation, in: updatedString)
354+
.map(isWordLike) ?? false
355+
let afterIsWordLike = characterAtUTF16Offset(insertionLocation, in: updatedString)
356+
.map(isWordLike) ?? false
367357

368358
var output = replacement
369359
if beforeIsWordLike && !hasLeadingWhitespace {
@@ -386,10 +376,11 @@ enum DiffRevertActionResolver {
386376
return range
387377
}
388378

389-
let hasLeadingWhitespace = range.location > 0
390-
&& isWhitespaceCharacter(updated.substring(with: NSRange(location: range.location - 1, length: 1)))
391-
let hasTrailingWhitespace = NSMaxRange(range) < updated.length
392-
&& isWhitespaceCharacter(updated.substring(with: NSRange(location: NSMaxRange(range), length: 1)))
379+
let updatedString = updated as String
380+
let hasLeadingWhitespace = characterBeforeUTF16Offset(range.location, in: updatedString)
381+
.map(isWhitespaceCharacter) ?? false
382+
let hasTrailingWhitespace = characterAtUTF16Offset(NSMaxRange(range), in: updatedString)
383+
.map(isWhitespaceCharacter) ?? false
393384

394385
if hasLeadingWhitespace, hasTrailingWhitespace {
395386
return NSRange(location: range.location, length: range.length + 1)
@@ -413,4 +404,26 @@ enum DiffRevertActionResolver {
413404
private static func isWordLike(_ scalarString: String) -> Bool {
414405
scalarString.rangeOfCharacter(from: .alphanumerics) != nil
415406
}
407+
408+
private static func characterBeforeUTF16Offset(_ offset: Int, in string: String) -> String? {
409+
guard offset > 0 else {
410+
return nil
411+
}
412+
let index = String.Index(utf16Offset: offset, in: string)
413+
guard index > string.startIndex else {
414+
return nil
415+
}
416+
return String(string[string.index(before: index)])
417+
}
418+
419+
private static func characterAtUTF16Offset(_ offset: Int, in string: String) -> String? {
420+
guard offset >= 0 else {
421+
return nil
422+
}
423+
let index = String.Index(utf16Offset: offset, in: string)
424+
guard index < string.endIndex else {
425+
return nil
426+
}
427+
return String(string[index])
428+
}
416429
}

Tests/TextDiffTests/DiffRevertActionResolverTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@ func standaloneDeletionAtEndRevertRestoresSpacing() throws {
138138
#expect(action.resultingUpdated == original)
139139
}
140140

141+
@Test
142+
func standaloneDeletionAfterSupplementaryPlaneLetterRestoresSpacing() throws {
143+
let original = "𐐀 cat"
144+
let updated = "𐐀"
145+
let segments = TextDiffEngine.diff(original: original, updated: updated, mode: .token)
146+
147+
let candidates = DiffRevertActionResolver.candidates(
148+
from: segments,
149+
mode: .token,
150+
original: original,
151+
updated: updated
152+
)
153+
let deletion = try #require(candidates.first(where: { $0.kind == .singleDeletion }))
154+
155+
let action = try #require(DiffRevertActionResolver.action(from: deletion, updated: updated))
156+
#expect(action.resultingUpdated == original)
157+
}
158+
141159
@Test
142160
func hyphenReplacingWhitespaceRevertRestoresOriginalSpacing() throws {
143161
let original = "in app purchase"

0 commit comments

Comments
 (0)