Skip to content

Commit b5a8ca9

Browse files
Rework Async tree-sitter Model, Fix Strong Ref Cycle (#225)
1 parent f76b48a commit b5a8ca9

24 files changed

+582
-629
lines changed

Package.resolved

+9-9
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
"kind" : "remoteSourceControl",
66
"location" : "https://github.com/CodeEditApp/CodeEditLanguages.git",
77
"state" : {
8-
"revision" : "af29ab4a15474a0a38ef88ef65c20e58a0812e43",
9-
"version" : "0.1.17"
8+
"revision" : "620b463c88894741e20d4711c9435b33547de5d2",
9+
"version" : "0.1.18"
1010
}
1111
},
1212
{
1313
"identity" : "codeedittextview",
1414
"kind" : "remoteSourceControl",
1515
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
1616
"state" : {
17-
"revision" : "c867fed329b2b4ce91a13742e20626f50cf233bb",
18-
"version" : "0.7.0"
17+
"revision" : "6abce20f1827a3665a5159195157f592352e38b4",
18+
"version" : "0.7.1"
1919
}
2020
},
2121
{
@@ -41,8 +41,8 @@
4141
"kind" : "remoteSourceControl",
4242
"location" : "https://github.com/apple/swift-collections.git",
4343
"state" : {
44-
"revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307",
45-
"version" : "1.0.5"
44+
"revision" : "d029d9d39c87bed85b1c50adee7c41795261a192",
45+
"version" : "1.0.6"
4646
}
4747
},
4848
{
@@ -57,10 +57,10 @@
5757
{
5858
"identity" : "swifttreesitter",
5959
"kind" : "remoteSourceControl",
60-
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter",
60+
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git",
6161
"state" : {
62-
"revision" : "df25a52f72ebc5b50ae20d26d1363793408bb28b",
63-
"version" : "0.7.1"
62+
"revision" : "2599e95310b3159641469d8a21baf2d3d200e61f",
63+
"version" : "0.8.0"
6464
}
6565
},
6666
{

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let package = Package(
2222
// tree-sitter languages
2323
.package(
2424
url: "https://github.com/CodeEditApp/CodeEditLanguages.git",
25-
exact: "0.1.17"
25+
exact: "0.1.18"
2626
),
2727
// SwiftLint
2828
.package(

Sources/CodeEditSourceEditor/CodeEditSourceEditor.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
212212
@MainActor
213213
public class Coordinator: NSObject {
214214
var parent: CodeEditSourceEditor
215-
var controller: TextViewController?
215+
weak var controller: TextViewController?
216216
var isUpdatingFromRepresentable: Bool = false
217217
var isUpdateFromTextView: Bool = false
218218

Sources/CodeEditSourceEditor/Controller/TextViewController+HighlightBracket.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ extension TextViewController {
1515
for range in textView.selectionManager.textSelections.map({ $0.range }) {
1616
if range.isEmpty,
1717
range.location > 0, // Range is not the beginning of the document
18-
let preceedingCharacter = textView.textStorage.substring(
18+
let precedingCharacter = textView.textStorage.substring(
1919
from: NSRange(location: range.location - 1, length: 1) // The preceding character exists
2020
) {
2121
for pair in BracketPairs.allValues {
22-
if preceedingCharacter == pair.0 {
22+
if precedingCharacter == pair.0 {
2323
// Walk forwards
2424
if let characterIndex = findClosingPair(
2525
pair.0,
@@ -34,7 +34,7 @@ extension TextViewController {
3434
highlightCharacter(range.location - 1)
3535
}
3636
}
37-
} else if preceedingCharacter == pair.1 && range.location - 1 > 0 {
37+
} else if precedingCharacter == pair.1 && range.location - 1 > 0 {
3838
// Walk backwards
3939
if let characterIndex = findClosingPair(
4040
pair.1,

Sources/CodeEditSourceEditor/Controller/TextViewController+Highlighter.swift

+2-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,8 @@ extension TextViewController {
3232
if let highlightProvider = highlightProvider {
3333
provider = highlightProvider
3434
} else {
35-
let textProvider: ResolvingQueryCursor.TextProvider = { [weak self] range, _ -> String? in
36-
return self?.textView.textStorage.mutableString.substring(with: range)
37-
}
38-
39-
provider = TreeSitterClient(textProvider: textProvider)
35+
self.treeSitterClient = TreeSitterClient()
36+
provider = self.treeSitterClient!
4037
}
4138

4239
if let provider = provider {

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,12 @@ public class TextViewController: NSViewController {
149149
}
150150
}
151151

152-
internal var highlighter: Highlighter?
152+
var highlighter: Highlighter?
153+
154+
/// The tree sitter client managed by the source editor.
155+
///
156+
/// This will be `nil` if another highlighter provider is passed to the source editor.
157+
internal(set) public var treeSitterClient: TreeSitterClient?
153158

154159
private var fontCharWidth: CGFloat { (" " as NSString).size(withAttributes: [.font: font]).width }
155160

Sources/CodeEditSourceEditor/Enums/CaptureName.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//
77

88
/// A collection of possible capture names for `tree-sitter` with their respected raw values.
9-
public enum CaptureName: String, CaseIterable {
9+
public enum CaptureName: String, CaseIterable, Sendable {
1010
case include
1111
case constructor
1212
case keyword

Sources/CodeEditSourceEditor/Extensions/NSRange+/NSRange+InputEdit.swift

+13-10
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@
66
//
77

88
import Foundation
9+
import CodeEditTextView
910
import SwiftTreeSitter
1011

1112
extension InputEdit {
12-
init?(range: NSRange, delta: Int, oldEndPoint: Point) {
13+
init?(range: NSRange, delta: Int, oldEndPoint: Point, textView: TextView) {
1314
let newEndLocation = NSMaxRange(range) + delta
1415

1516
if newEndLocation < 0 {
1617
assertionFailure("Invalid range/delta")
1718
return nil
1819
}
1920

20-
// TODO: - Ask why Neon only uses .zero for these
21-
let startPoint: Point = .zero
22-
let newEndPoint: Point = .zero
21+
let newRange = NSRange(location: range.location, length: range.length + delta)
22+
let startPoint = textView.pointForLocation(newRange.location) ?? .zero
23+
let newEndPoint = textView.pointForLocation(newEndLocation) ?? .zero
2324

24-
self.init(startByte: UInt32(range.location * 2),
25-
oldEndByte: UInt32(NSMaxRange(range) * 2),
26-
newEndByte: UInt32(newEndLocation * 2),
27-
startPoint: startPoint,
28-
oldEndPoint: oldEndPoint,
29-
newEndPoint: newEndPoint)
25+
self.init(
26+
startByte: UInt32(range.location * 2),
27+
oldEndByte: UInt32(NSMaxRange(range) * 2),
28+
newEndByte: UInt32(newEndLocation * 2),
29+
startPoint: startPoint,
30+
oldEndPoint: oldEndPoint,
31+
newEndPoint: newEndPoint
32+
)
3033
}
3134
}
3235

Sources/CodeEditSourceEditor/Extensions/Parser+createTree.swift

-20
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// TextView+Point.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 1/18/24.
6+
//
7+
8+
import Foundation
9+
import CodeEditTextView
10+
import SwiftTreeSitter
11+
12+
extension TextView {
13+
func pointForLocation(_ location: Int) -> Point? {
14+
guard let linePosition = layoutManager.textLineForOffset(location) else { return nil }
15+
let column = location - linePosition.range.location
16+
return Point(row: linePosition.index, column: column)
17+
}
18+
}

Sources/CodeEditSourceEditor/Extensions/HighlighterTextView+createReadBlock.swift renamed to Sources/CodeEditSourceEditor/Extensions/TextView+/TextView+createReadBlock.swift

+12-5
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,27 @@
66
//
77

88
import Foundation
9+
import CodeEditTextView
910
import SwiftTreeSitter
1011

11-
extension HighlighterTextView {
12+
extension TextView {
1213
func createReadBlock() -> Parser.ReadBlock {
13-
return { byteOffset, _ in
14-
let limit = self.documentRange.length
14+
return { [weak self] byteOffset, _ in
15+
let limit = self?.documentRange.length ?? 0
1516
let location = byteOffset / 2
1617
let end = min(location + (1024), limit)
17-
if location > end {
18+
if location > end || self == nil {
1819
// Ignore and return nothing, tree-sitter's internal tree can be incorrect in some situations.
1920
return nil
2021
}
2122
let range = NSRange(location..<end)
22-
return self.stringForRange(range)?.data(using: String.nativeUTF16Encoding)
23+
return self?.stringForRange(range)?.data(using: String.nativeUTF16Encoding)
24+
}
25+
}
26+
27+
func createReadCallback() -> SwiftTreeSitter.Predicate.TextProvider {
28+
return { [weak self] range, _ in
29+
return self?.stringForRange(range)
2330
}
2431
}
2532
}

Sources/CodeEditSourceEditor/Highlighting/HighlightProviding.swift

+19-18
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,43 @@
66
//
77

88
import Foundation
9+
import CodeEditTextView
910
import CodeEditLanguages
1011
import AppKit
1112

1213
/// The protocol a class must conform to to be used for highlighting.
1314
public protocol HighlightProviding: AnyObject {
14-
/// A unique identifier for the highlighter object.
15-
/// Example: `"CodeEdit.TreeSitterHighlighter"`
16-
/// - Note: This does not need to be *globally* unique, merely unique across all the highlighters used.
17-
var identifier: String { get }
18-
1915
/// Called once to set up the highlight provider with a data source and language.
2016
/// - Parameters:
2117
/// - textView: The text view to use as a text source.
22-
/// - codeLanguage: The langugage that should be used by the highlighter.
23-
func setUp(textView: HighlighterTextView, codeLanguage: CodeLanguage)
18+
/// - codeLanguage: The language that should be used by the highlighter.
19+
func setUp(textView: TextView, codeLanguage: CodeLanguage)
20+
21+
/// Notifies the highlighter that an edit is going to happen in the given range.
22+
/// - Parameters:
23+
/// - textView: The text view to use.
24+
/// - range: The range of the incoming edit.
25+
func willApplyEdit(textView: TextView, range: NSRange)
2426

2527
/// Notifies the highlighter of an edit and in exchange gets a set of indices that need to be re-highlighted.
2628
/// The returned `IndexSet` should include all indexes that need to be highlighted, including any inserted text.
2729
/// - Parameters:
28-
/// - textView:The text view to use.
30+
/// - textView: The text view to use.
2931
/// - range: The range of the edit.
3032
/// - delta: The length of the edit, can be negative for deletions.
31-
/// - completion: The function to call with an `IndexSet` containing all Indices to invalidate.
32-
func applyEdit(textView: HighlighterTextView,
33-
range: NSRange,
34-
delta: Int,
35-
completion: @escaping ((IndexSet) -> Void))
33+
/// - Returns: an `IndexSet` containing all Indices to invalidate.
34+
func applyEdit(textView: TextView, range: NSRange, delta: Int, completion: @escaping (IndexSet) -> Void)
3635

3736
/// Queries the highlight provider for any ranges to apply highlights to. The highlight provider should return an
3837
/// array containing all ranges to highlight, and the capture type for the range. Any ranges or indexes
3938
/// excluded from the returned array will be treated as plain text and highlighted as such.
4039
/// - Parameters:
4140
/// - textView: The text view to use.
42-
/// - range: The range to operate on.
43-
/// - completion: Function to call with all ranges to highlight
44-
func queryHighlightsFor(textView: HighlighterTextView,
45-
range: NSRange,
46-
completion: @escaping (([HighlightRange]) -> Void))
41+
/// - range: The range to query.
42+
/// - Returns: All highlight ranges for the queried ranges.
43+
func queryHighlightsFor(textView: TextView, range: NSRange, completion: @escaping ([HighlightRange]) -> Void)
44+
}
45+
46+
extension HighlightProviding {
47+
public func willApplyEdit(textView: TextView, range: NSRange) { }
4748
}

Sources/CodeEditSourceEditor/Highlighting/HighlightRange.swift

+2-7
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@
77

88
import Foundation
99

10-
/// This class represents a range to highlight, as well as the capture name for syntax coloring.
11-
public class HighlightRange {
12-
init(range: NSRange, capture: CaptureName?) {
13-
self.range = range
14-
self.capture = capture
15-
}
16-
10+
/// This struct represents a range to highlight, as well as the capture name for syntax coloring.
11+
public struct HighlightRange: Sendable {
1712
let range: NSRange
1813
let capture: CaptureName?
1914
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Highlighter+NSTextStorageDelegate.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 1/18/24.
6+
//
7+
8+
import AppKit
9+
10+
extension Highlighter: NSTextStorageDelegate {
11+
/// Processes an edited range in the text.
12+
/// Will query tree-sitter for any updated indices and re-highlight only the ranges that need it.
13+
func textStorage(
14+
_ textStorage: NSTextStorage,
15+
didProcessEditing editedMask: NSTextStorageEditActions,
16+
range editedRange: NSRange,
17+
changeInLength delta: Int
18+
) {
19+
// This method is called whenever attributes are updated, so to avoid re-highlighting the entire document
20+
// each time an attribute is applied, we check to make sure this is in response to an edit.
21+
guard editedMask.contains(.editedCharacters) else { return }
22+
23+
self.storageDidEdit(editedRange: editedRange, delta: delta)
24+
}
25+
26+
func textStorage(
27+
_ textStorage: NSTextStorage,
28+
willProcessEditing editedMask: NSTextStorageEditActions,
29+
range editedRange: NSRange,
30+
changeInLength delta: Int
31+
) {
32+
guard editedMask.contains(.editedCharacters) else { return }
33+
34+
self.storageWillEdit(editedRange: editedRange)
35+
}
36+
}

0 commit comments

Comments
 (0)