Skip to content

Commit bc422b4

Browse files
authored
Highlight Ignored Indexes (CodeEditApp#114)
# Description Tracks indexes that were not given an explicit color and highlights them as normal text. This fixes problems where certain characters would change color (eg: they were put in a comment) and then need to change back, but were skipped. This also fixes the problem in CodeEditApp#99 where text was not given an explicit highlight on paste. [Since `STTextView` does not add default attributes to pasted text by default](https://github.com/krzyzanowskim/STTextView/blob/5d137731401d12412d567244facf086c325ff95b/Sources/STTextView/STTextView%2BCopyPaste.swift#L26) (see the `useTypingAttributes: false` in that method call). Doing this also seems to have an effect on the annoying glitching that was caused when entering text in an empty line (see the comment screen recording) This PR also adds a helper for converting from an `NSRange` to a `Range<Int>` and gets rid of a bunch of ugly force unwraps that existed before. It also adds some convenience methods for modifying `IndexSet`s using indexes from `NSRange` objects. # Related Issues - CodeEditApp#99 # UI Fixes ### Paste Text (Old) https://user-images.githubusercontent.com/35942988/212186310-ee50dcaa-ebec-4e21-905b-562fc2cdf940.mov ### Paste Text (new) https://user-images.githubusercontent.com/35942988/212186342-402ceb89-2122-4431-88ac-2363e6452323.mov ### Multi-Line comment (old) https://user-images.githubusercontent.com/35942988/212186411-dc77744f-270b-4615-80c6-ed02e7c38e80.mov ### Multi-Line comment (new) https://user-images.githubusercontent.com/35942988/212186437-ba014944-fa6c-4de9-8ad3-c8105a655418.mov Closes CodeEditApp#99
2 parents a0f2a19 + 618ba0e commit bc422b4

File tree

2 files changed

+68
-21
lines changed

2 files changed

+68
-21
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// IndexSet+NSRange.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/12/23.
6+
//
7+
8+
import Foundation
9+
10+
extension NSRange {
11+
/// Convenience getter for safely creating a `Range<Int>` from an `NSRange`
12+
var intRange: Range<Int> {
13+
self.location..<NSMaxRange(self)
14+
}
15+
}
16+
17+
/// Helpers for working with `NSRange`s and `IndexSet`s.
18+
extension IndexSet {
19+
/// Initializes the index set with a range of integers
20+
init(integersIn range: NSRange) {
21+
self.init(integersIn: range.intRange)
22+
}
23+
24+
/// Remove all the integers in the `NSRange`
25+
mutating func remove(integersIn range: NSRange) {
26+
self.remove(integersIn: range.intRange)
27+
}
28+
29+
/// Insert all the integers in the `NSRange`
30+
mutating func insert(integersIn range: NSRange) {
31+
self.insert(integersIn: range.intRange)
32+
}
33+
34+
/// Returns true if self contains all of the integers in range.
35+
func contains(integersIn range: NSRange) -> Bool {
36+
return self.contains(integersIn: range.intRange)
37+
}
38+
}

Sources/CodeEditTextView/Highlighting/Highlighter.swift

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ class Highlighter: NSObject {
4242

4343
/// The set of visible indexes in tht text view
4444
lazy private var visibleSet: IndexSet = {
45-
guard let range = textView.visibleTextRange else {
46-
return IndexSet()
47-
}
48-
return IndexSet(integersIn: Range(range)!)
45+
return IndexSet(integersIn: textView.visibleTextRange ?? NSRange())
4946
}()
5047

5148
// MARK: - UI
@@ -107,7 +104,7 @@ class Highlighter: NSObject {
107104
if !(treeSitterClient?.hasSetText ?? true) {
108105
treeSitterClient?.setText(text: textView.string)
109106
}
110-
invalidate(range: entireTextRange)
107+
invalidate(range: NSRange(entireTextRange))
111108
}
112109

113110
/// Sets the language and causes a re-highlight of the entire text.
@@ -129,12 +126,6 @@ private extension Highlighter {
129126
/// Invalidates a given range and adds it to the queue to be highlighted.
130127
/// - Parameter range: The range to invalidate.
131128
func invalidate(range: NSRange) {
132-
invalidate(range: Range(range)!)
133-
}
134-
135-
/// Invalidates a given range and adds it to the queue to be highlighted.
136-
/// - Parameter range: The range to invalidate.
137-
func invalidate(range: Range<Int>) {
138129
let set = IndexSet(integersIn: range)
139130

140131
if set.isEmpty {
@@ -161,31 +152,35 @@ private extension Highlighter {
161152

162153
/// Highlights the given range
163154
/// - Parameter range: The range to request highlights for.
164-
func highlight(range nsRange: NSRange) {
165-
let range = Range(nsRange)!
166-
pendingSet.insert(integersIn: range)
155+
func highlight(range rangeToHighlight: NSRange) {
156+
pendingSet.insert(integersIn: rangeToHighlight)
167157

168-
treeSitterClient?.queryColorsFor(range: nsRange) { [weak self] highlightRanges in
158+
treeSitterClient?.queryColorsFor(range: rangeToHighlight) { [weak self] highlightRanges in
169159
guard let attributeProvider = self?.attributeProvider,
170160
let textView = self?.textView else { return }
171161

172162
// Mark these indices as not pending and valid
173-
self?.pendingSet.remove(integersIn: range)
174-
self?.validSet.formUnion(IndexSet(integersIn: range))
163+
self?.pendingSet.remove(integersIn: rangeToHighlight)
164+
self?.validSet.formUnion(IndexSet(integersIn: rangeToHighlight))
175165

176166
// If this range does not exist in the visible set, we can exit.
177-
if !(self?.visibleSet ?? .init()).contains(integersIn: range) {
167+
if !(self?.visibleSet ?? .init()).contains(integersIn: rangeToHighlight) {
178168
return
179169
}
180170

181171
// Try to create a text range for invalidating. If this fails we fail silently
182172
guard let textContentManager = textView.textLayoutManager.textContentManager,
183-
let textRange = NSTextRange(nsRange, provider: textContentManager) else {
173+
let textRange = NSTextRange(rangeToHighlight, provider: textContentManager) else {
184174
return
185175
}
186176

187177
// Loop through each highlight and modify the textStorage accordingly.
188178
textView.textContentStorage.textStorage?.beginEditing()
179+
180+
// Create a set of indexes that were not highlighted.
181+
var ignoredIndexes = IndexSet(integersIn: rangeToHighlight)
182+
183+
// Apply all highlights that need color
189184
for highlight in highlightRanges {
190185
// Does not work:
191186
// textView.textLayoutManager.setRenderingAttributes(attributeProvider.attributesFor(highlight.capture),
@@ -196,7 +191,21 @@ private extension Highlighter {
196191
attributeProvider.attributesFor(highlight.capture),
197192
range: highlight.range
198193
)
194+
195+
// Remove highlighted indexes from the "ignored" indexes.
196+
ignoredIndexes.remove(integersIn: highlight.range)
197+
}
198+
199+
// For any indices left over, we need to apply normal attributes to them
200+
// This fixes the case where characters are changed to have a non-text color, and then are skipped when
201+
// they need to be changed back.
202+
for ignoredRange in ignoredIndexes.rangeView {
203+
textView.textContentStorage.textStorage?.setAttributes(
204+
attributeProvider.attributesFor(nil),
205+
range: NSRange(ignoredRange)
206+
)
199207
}
208+
200209
textView.textContentStorage.textStorage?.endEditing()
201210

202211
// After applying edits to the text storage we need to invalidate the layout
@@ -227,7 +236,7 @@ private extension Highlighter {
227236
private extension Highlighter {
228237
/// Updates the view to highlight newly visible text when the textview is scrolled or bounds change.
229238
@objc func visibleTextChanged(_ notification: Notification) {
230-
visibleSet = IndexSet(integersIn: Range(textView.visibleTextRange ?? NSRange())!)
239+
visibleSet = IndexSet(integersIn: textView.visibleTextRange ?? NSRange())
231240

232241
// Any indices that are both *not* valid and in the visible text range should be invalidated
233242
let newlyInvalidSet = visibleSet.subtracting(validSet)
@@ -263,7 +272,7 @@ extension Highlighter: NSTextStorageDelegate {
263272
treeSitterClient?.applyEdit(edit,
264273
text: textStorage.string) { [weak self] invalidatedIndexSet in
265274
let indexSet = invalidatedIndexSet
266-
.union(IndexSet(integersIn: Range(editedRange)!))
275+
.union(IndexSet(integersIn: editedRange))
267276
// Only invalidate indices that aren't visible.
268277
.intersection(self?.visibleSet ?? .init())
269278

0 commit comments

Comments
 (0)