Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TreeSitter Performance And Stability #263

Merged
merged 11 commits into from
Sep 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
"state" : {
"revision" : "eb1d38247a45bc678b5a23a65d6f6df6c56519e4",
"version" : "0.7.5"
"revision" : "2619cb945b4d6c2fc13f22ba873ba891f552b0f3",
"version" : "0.7.6"
}
},
{
Expand All @@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.1"
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.1.3"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct ContentView: View {
@AppStorage("wrapLines") private var wrapLines: Bool = true
@State private var cursorPositions: [CursorPosition] = []
@AppStorage("systemCursor") private var useSystemCursor: Bool = false
@State private var isInLongParse = false

init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
self._document = document
Expand Down Expand Up @@ -47,21 +48,47 @@ struct ContentView: View {
.zIndex(2)
.background(Color(NSColor.windowBackgroundColor))
Divider()
CodeEditSourceEditor(
$document.text,
language: language,
theme: theme,
font: font,
tabWidth: 4,
lineHeight: 1.2,
wrapLines: wrapLines,
cursorPositions: $cursorPositions,
useSystemCursor: useSystemCursor
)
ZStack {
if isInLongParse {
VStack {
HStack {
Spacer()
Text("Parsing document...")
Spacer()
}
.padding(4)
.background(Color(NSColor.windowBackgroundColor))
Spacer()
}
.zIndex(2)
.transition(.opacity)
}
CodeEditSourceEditor(
$document.text,
language: language,
theme: theme,
font: font,
tabWidth: 4,
lineHeight: 1.2,
wrapLines: wrapLines,
cursorPositions: $cursorPositions,
useSystemCursor: useSystemCursor
)
}
}
.onAppear {
self.language = detectLanguage(fileURL: fileURL) ?? .default
}
.onReceive(NotificationCenter.default.publisher(for: TreeSitterClient.Constants.longParse)) { _ in
withAnimation(.easeIn(duration: 0.1)) {
isInLongParse = true
}
}
.onReceive(NotificationCenter.default.publisher(for: TreeSitterClient.Constants.longParseFinished)) { _ in
withAnimation(.easeIn(duration: 0.1)) {
isInLongParse = false
}
}
}

private func detectLanguage(fileURL: URL?) -> CodeLanguage? {
Expand All @@ -87,7 +114,7 @@ struct ContentView: View {
}

// When there's a single cursor, display the line and column.
return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column)"
return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column) Range: \(cursorPositions[0].range)"
}
}

Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ let package = Package(
dependencies: [
"CodeEditTextView",
"CodeEditLanguages",
"TextFormation",
"TextFormation"
],
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// DispatchQueue+dispatchMainIfNot.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 9/2/24.
//

import Foundation

/// Helper methods for dispatching (sync or async) on the main queue only if the calling thread is not already the
/// main queue.

extension DispatchQueue {
/// Executes the work item on the main thread, dispatching asynchronously if the thread is not the main thread.
/// - Parameter item: The work item to execute on the main thread.
static func dispatchMainIfNot(_ item: @escaping () -> Void) {
if Thread.isMainThread {
item()
} else {
DispatchQueue.main.async {
item()
}
}
}

/// Executes the work item on the main thread, keeping control on the calling thread until the work item is
/// executed if not already on the main thread.
/// - Parameter item: The work item to execute.
/// - Returns: The value of the work item.
static func syncMainIfNot<T>(_ item: @escaping () -> T) -> T {
if Thread.isMainThread {
return item()
} else {
return DispatchQueue.main.sync {
return item()
}
}
}
}
27 changes: 27 additions & 0 deletions Sources/CodeEditSourceEditor/Extensions/Result+ThrowOrReturn.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Result+ThrowOrReturn.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 9/2/24.
//

import Foundation

extension Result {
func throwOrReturn() throws -> Success {
switch self {
case let .success(success):
return success
case let .failure(failure):
throw failure
}
}

var isSuccess: Bool {
if case .success = self {
return true
} else {
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,42 @@ import CodeEditTextView
import SwiftTreeSitter

extension TextView {
/// Creates a block for safely reading data into a parser's read block.
///
/// If the thread is the main queue, executes synchronously.
/// Otherwise it will block the calling thread and execute the block on the main queue, returning control to the
/// calling queue when the block is finished running.
///
/// - Returns: A new block for reading contents for tree-sitter.
func createReadBlock() -> Parser.ReadBlock {
return { [weak self] byteOffset, _ in
let limit = self?.documentRange.length ?? 0
let location = byteOffset / 2
let end = min(location + (1024), limit)
if location > end || self == nil {
// Ignore and return nothing, tree-sitter's internal tree can be incorrect in some situations.
return nil
let workItem: () -> Data? = {
let limit = self?.documentRange.length ?? 0
let location = byteOffset / 2
let end = min(location + (TreeSitterClient.Constants.charsToReadInBlock), limit)
if location > end || self == nil {
// Ignore and return nothing, tree-sitter's internal tree can be incorrect in some situations.
return nil
}
let range = NSRange(location..<end)
return self?.stringForRange(range)?.data(using: String.nativeUTF16Encoding)
}
let range = NSRange(location..<end)
return self?.stringForRange(range)?.data(using: String.nativeUTF16Encoding)
return DispatchQueue.syncMainIfNot(workItem)
}
}

/// Creates a block for safely reading data for a text provider.
///
/// If the thread is the main queue, executes synchronously.
/// Otherwise it will block the calling thread and execute the block on the main queue, returning control to the
/// calling queue when the block is finished running.
///
/// - Returns: A new block for reading contents for tree-sitter.
func createReadCallback() -> SwiftTreeSitter.Predicate.TextProvider {
return { [weak self] range, _ in
return self?.stringForRange(range)
let workItem: () -> String? = {
self?.stringForRange(range)
}
return DispatchQueue.syncMainIfNot(workItem)
}
}
}
24 changes: 21 additions & 3 deletions Sources/CodeEditSourceEditor/Highlighting/HighlightProviding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@ import CodeEditTextView
import CodeEditLanguages
import AppKit

/// A single-case error that should be thrown when an operation should be retried.
public enum HighlightProvidingError: Error {
case operationCancelled
}

/// The protocol a class must conform to to be used for highlighting.
public protocol HighlightProviding: AnyObject {
/// Called once to set up the highlight provider with a data source and language.
/// - Parameters:
/// - textView: The text view to use as a text source.
/// - codeLanguage: The language that should be used by the highlighter.
@MainActor
func setUp(textView: TextView, codeLanguage: CodeLanguage)

/// Notifies the highlighter that an edit is going to happen in the given range.
/// - Parameters:
/// - textView: The text view to use.
/// - range: The range of the incoming edit.
@MainActor
func willApplyEdit(textView: TextView, range: NSRange)

/// Notifies the highlighter of an edit and in exchange gets a set of indices that need to be re-highlighted.
Expand All @@ -30,8 +37,14 @@ public protocol HighlightProviding: AnyObject {
/// - textView: The text view to use.
/// - range: The range of the edit.
/// - delta: The length of the edit, can be negative for deletions.
/// - Returns: an `IndexSet` containing all Indices to invalidate.
func applyEdit(textView: TextView, range: NSRange, delta: Int, completion: @escaping (IndexSet) -> Void)
/// - Returns: An `IndexSet` containing all Indices to invalidate.
@MainActor
func applyEdit(
textView: TextView,
range: NSRange,
delta: Int,
completion: @escaping @MainActor (Result<IndexSet, Error>) -> Void
)

/// Queries the highlight provider for any ranges to apply highlights to. The highlight provider should return an
/// array containing all ranges to highlight, and the capture type for the range. Any ranges or indexes
Expand All @@ -40,7 +53,12 @@ public protocol HighlightProviding: AnyObject {
/// - textView: The text view to use.
/// - range: The range to query.
/// - Returns: All highlight ranges for the queried ranges.
func queryHighlightsFor(textView: TextView, range: NSRange, completion: @escaping ([HighlightRange]) -> Void)
@MainActor
func queryHighlightsFor(
textView: TextView,
range: NSRange,
completion: @escaping @MainActor (Result<[HighlightRange], Error>) -> Void
)
}

extension HighlightProviding {
Expand Down
Loading
Loading