diff --git a/Sources/Neon/Highlighter.swift b/Sources/Neon/Highlighter.swift index cd59a18..f3b3463 100644 --- a/Sources/Neon/Highlighter.swift +++ b/Sources/Neon/Highlighter.swift @@ -59,13 +59,34 @@ extension Highlighter { } extension Highlighter { + /// Calculates any newly-visible text that is invalid + /// + /// You should invoke this method when the visible text + /// in your system changes. public func visibleContentDidChange() { let set = invalidSet.intersection(visibleSet) invalidate(.set(set)) } - public func didChangeContent(in range: NSRange, delta: Int, limit: Int) { + /// Update internal state in response to an edit. + /// + /// This method must be invoked on every text change. The `range` + /// parameter must refer to the range of text that **was** changed. + /// Consider the example text `"abc"`. + /// + /// Inserting a "d" at the end: + /// + /// range = NSRange(3..<3) + /// delta = 1 + /// + /// Deleting the middle "b": + /// + /// range = NSRange(1..<2) + /// delta = -1 + public func didChangeContent(in range: NSRange, delta: Int) { + let limit = textLength - delta + let mutation = RangeMutation(range: range, delta: delta, limit: limit) self.validSet = mutation.transform(set: validSet) diff --git a/Sources/Neon/TextContainerSystemInterface.swift b/Sources/Neon/TextContainerSystemInterface.swift index 486a3fb..8e8650c 100644 --- a/Sources/Neon/TextContainerSystemInterface.swift +++ b/Sources/Neon/TextContainerSystemInterface.swift @@ -45,10 +45,14 @@ public struct TextContainerSystemInterface { extension TextContainerSystemInterface: TextSystemInterface { public func clearStyle(in range: NSRange) { + assert(range.max <= length, "range is out of bounds, is the text state being updated correctly?") + layoutManager?.setTemporaryAttributes([:], forCharacterRange: range) } public func applyStyle(to token: Token) { + assert(token.range.max <= length, "range is out of bounds, is the text state being updated correctly?") + guard let attrs = attributeProvider(token) else { return } layoutManager?.setTemporaryAttributes(attrs, forCharacterRange: token.range) diff --git a/Tests/NeonTests/HighlighterTests.swift b/Tests/NeonTests/HighlighterTests.swift new file mode 100644 index 0000000..0f25232 --- /dev/null +++ b/Tests/NeonTests/HighlighterTests.swift @@ -0,0 +1,40 @@ +import XCTest +import Neon + +class MockInterface: TextSystemInterface { + var length: Int + var visibleRange: NSRange + + init(length: Int = 0, visibleRange: NSRange = .zero) { + self.length = length + self.visibleRange = visibleRange + } + + func clearStyle(in range: NSRange) { + } + + func applyStyle(to token: Token) { + } +} + +class HighlighterTests: XCTestCase { + func testEditAndVisibleRangeChange() throws { + let interface = MockInterface() + + var requestedRange: NSRange? = nil + let provider: TokenProvider = { range, block in + requestedRange = range + block(.success(.noChange)) + } + + let highlighter = Highlighter(textInterface: interface, tokenProvider: provider) + + interface.length = 10 + highlighter.didChangeContent(in: NSRange(0..<0), delta: 10) + + interface.visibleRange = NSRange(0..<10) + highlighter.visibleContentDidChange() + + XCTAssertEqual(requestedRange, NSRange(0..<10)) + } +}