Skip to content

Commit

Permalink
It works
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Nov 26, 2024
1 parent f88fef0 commit 26db3fe
Show file tree
Hide file tree
Showing 20 changed files with 401 additions and 288 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
5 changes: 2 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "9e30aec45ad85758dfd7ac93c22c9e993946ae0153fd490208c3a04094f4813f",
"originHash" : "a818c4732d60e59901e640cf8c5de3900e8ec0ffaa1daa0a631b8676b0b17c4b",
"pins" : [
{
"identity" : "rearrange",
Expand All @@ -15,8 +15,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter",
"state" : {
"revision" : "36aa61d1b531f744f35229f010efba9c6d6cbbdd",
"version" : "0.9.0"
"revision" : "55f2c2fdaf859f86e4ea8513b9934badc7894019"
}
},
{
Expand Down
14 changes: 7 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
.library(name: "Neon", targets: ["Neon"]),
],
dependencies: [
.package(url: "https://github.com/ChimeHQ/SwiftTreeSitter", from: "0.9.0"),
.package(url: "https://github.com/ChimeHQ/SwiftTreeSitter", revision: "55f2c2fdaf859f86e4ea8513b9934badc7894019"),
.package(url: "https://github.com/ChimeHQ/Rearrange", from: "2.0.0"),
],
targets: [
Expand All @@ -24,19 +24,19 @@ let package = Package(
.target(
name: "Neon",
dependencies: [
"RangeState",
"Rearrange",
"TreeSitterClient",
.product(name: "SwiftTreeSitterLayer", package: "SwiftTreeSitter"),
]
"RangeState",
"Rearrange",
"TreeSitterClient",
.product(name: "SwiftTreeSitterLayer", package: "SwiftTreeSitter"),
]
),
.target(
name: "TreeSitterClient",
dependencies: [
"RangeState",
"Rearrange",
"SwiftTreeSitter",
.product(name: "SwiftTreeSitterLayer", package: "SwiftTreeSitter"),
.product(name: "SwiftTreeSitterLayer", package: "SwiftTreeSitter"),
]
),
.target(
Expand Down
8 changes: 8 additions & 0 deletions Projects/NeonExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
TVOS_DEPLOYMENT_TARGET = 16.0;
WATCHOS_DEPLOYMENT_TARGET = 9.0;
};
name = Debug;
};
Expand All @@ -275,6 +279,10 @@
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
TVOS_DEPLOYMENT_TARGET = 16.0;
WATCHOS_DEPLOYMENT_TARGET = 9.0;
};
name = Release;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "88a29eea1c1b10215ec2071d3cc04cae5d3b2418319c7e017e284ba383823031",
"pins" : [
{
"identity" : "nsui",
Expand All @@ -13,16 +14,25 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/Rearrange",
"state" : {
"revision" : "5ff7f3363f7a08f77e0d761e38e6add31c2136e1",
"version" : "1.8.1"
"revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3",
"version" : "2.0.0"
}
},
{
"identity" : "swifttreesitter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter",
"state" : {
"revision" : "b01904a3737649c1d8520106bbb285724fe5b0bb"
"revision" : "55f2c2fdaf859f86e4ea8513b9934badc7894019"
}
},
{
"identity" : "tree-sitter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tree-sitter/tree-sitter",
"state" : {
"revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e",
"version" : "0.23.2"
}
},
{
Expand All @@ -44,5 +54,5 @@
}
}
],
"version" : 2
"version" : 3
}
14 changes: 5 additions & 9 deletions Projects/NeonExample/TextViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ final class TextViewController: NSUIViewController {
private let highlighter: TextViewHighlighter

init() {
if #available(iOS 16.0, *) {
self.textView = NSUITextView(usingTextLayoutManager: false)
} else {
self.textView = NSUITextView()
}
self.textView = NSUITextView(usingTextLayoutManager: false)

self.highlighter = try! Self.makeHighlighter(for: textView)

super.init(nibName: nil, bundle: nil)

// enable non-continguous layout for TextKit 1
if #available(macOS 12.0, iOS 16.0, tvOS 15.0, *), textView.textLayoutManager == nil {
if textView.textLayoutManager == nil {
textView.nsuiLayoutManager?.allowsNonContiguousLayout = true
}
}
Expand Down Expand Up @@ -119,17 +115,17 @@ final class TextViewController: NSUIViewController {
// it wasn't that way on creation
highlighter.observeEnclosingScrollView()

regularTest()
regularTestWithSwiftCode()
}

func regularTest() {
func regularTestWithSwiftCode() {
let url = Bundle.main.url(forResource: "test", withExtension: "code")!
let content = try! String(contentsOf: url)

textView.text = content
}

func doBigTest() {
func doBigMarkdownTest() {
let url = Bundle.main.url(forResource: "big_test", withExtension: "md")!
let content = try! String(contentsOf: url)

Expand Down
8 changes: 6 additions & 2 deletions Sources/Neon/TextSystemInterface+Validation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension TextSystemInterface {
mainActorAsyncValue: { contentRange in
await asyncValidate(
contentRange,
provider: { await provider.async(isolation: MainActor.shared, $0) }
provider: { range in await provider.async(range) }
)
}
)
Expand All @@ -72,6 +72,10 @@ extension TextSystemInterface {
func validatorSecondaryHandler(
with provider: @escaping Styler.SecondaryValidationProvider
) -> SecondaryValidationProvider {
{ await asyncValidate($0, provider: provider) }
{ range in
await asyncValidate(range, provider: {
await provider($0)
})
}
}
}
10 changes: 6 additions & 4 deletions Sources/Neon/TextViewHighlighter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ extension TextView {
/// A class that can connect `NSTextView`/`UITextView` to `TreeSitterClient`
///
/// This class is a minimal implementation that can help perform highlighting for a TextView. It is compatible with both TextKit 1 and 2 views, and uses single-phase pass with tree-sitter. The created instance will become the delegate of the view's `NSTextStorage`.
@available(macOS 13.0, macCatalyst 16.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
@MainActor
public final class TextViewHighlighter {
private typealias Styler = TextSystemStyler<TextViewSystemInterface>
Expand Down Expand Up @@ -87,11 +88,11 @@ public final class TextViewHighlighter {
configuration: .init(
languageProvider: configuration.languageProvider,
contentProvider: { [interface] in interface.languageLayerContent(with: $0) },
contentSnapshopProvider: { [interface] in interface.languageLayerContentSnapshot(with: $0) },
lengthProvider: { [interface] in interface.content.currentLength },
invalidationHandler: { [buffer] in buffer.invalidate(.set($0)) },
locationTransformer: configuration.locationTransformer
),
isolation: MainActor.shared
)
)

// this level of indirection is necessary so when the TextProvider is accessed it always uses the current version of the content
Expand Down Expand Up @@ -132,7 +133,7 @@ public final class TextViewHighlighter {

try textView.getTextStorage().delegate = storageDelegate

observeEnclosingScrollView()
observeEnclosingScrollView()

invalidate(.all)
}
Expand All @@ -148,6 +149,7 @@ public final class TextViewHighlighter {
}
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension TextViewHighlighter {
/// Begin monitoring for containing scroll view changes.
///
Expand All @@ -158,7 +160,7 @@ extension TextViewHighlighter {
print("warning: there is no enclosing scroll view")
return
}

NotificationCenter.default.addObserver(
self,
selector: #selector(visibleContentChanged(_:)),
Expand Down
9 changes: 5 additions & 4 deletions Sources/Neon/ThreePhaseTextSystemStyler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RangeState
@MainActor
public final class ThreePhaseTextSystemStyler<Interface: TextSystemInterface> {
public typealias FallbackTokenProvider = (NSRange) -> TokenApplication
public typealias SecondaryValidationProvider = @Sendable (NSRange) async -> TokenApplication
public typealias SecondaryValidationProvider = (NSRange) async -> TokenApplication

private let textSystem: Interface
private let validator: ThreePhaseRangeValidator<Interface.Content>
Expand All @@ -31,7 +31,8 @@ public final class ThreePhaseTextSystemStyler<Interface: TextSystemInterface> {
secondaryProvider: textSystem.validatorSecondaryHandler(with: secondaryValidationProvider),
secondaryValidationDelay: 3.0,
prioritySetProvider: { textSystem.visibleSet }
)
),
isolation: MainActor.shared
)
}

Expand All @@ -46,12 +47,12 @@ public final class ThreePhaseTextSystemStyler<Interface: TextSystemInterface> {
public func validate(_ target: RangeTarget) {
let prioritySet = textSystem.visibleSet

validator.validate(target, prioritizing: prioritySet)
validator.validate(target, prioritizing: prioritySet, isolation: MainActor.shared)
}

public func validate() {
let prioritySet = textSystem.visibleSet

validator.validate(.set(prioritySet), prioritizing: prioritySet)
validator.validate(.set(prioritySet), prioritizing: prioritySet, isolation: MainActor.shared)
}
}
12 changes: 12 additions & 0 deletions Sources/Neon/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,16 @@ extension TokenProvider {
}
)
}

/// A TokenProvider that returns an empty set of tokens for all async requests, but fails to resolve tokens synchronously.
public static var asyncOnlyNone: TokenProvider {
.init(
syncValue: { _ in
return nil
},
asyncValue: { _, _ in
return .noChange
}
)
}
}
33 changes: 18 additions & 15 deletions Sources/Neon/TreeSitterClient+Neon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,19 @@ extension TokenApplication {

self.init(tokens: tokens, range: range)
}

public init(namedRanges: [NamedRange], range: NSRange) {
let tokens = namedRanges.map {
let name = $0.name

return Token(name: name, range: $0.range)
}

self.init(tokens: tokens, range: range)
}
}

@available(macOS 13.0, macCatalyst 16.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension TreeSitterClient {
@MainActor
public func tokenProvider(with provider: @escaping TextProvider, nameMap: [String : String] = [:]) -> TokenProvider {
Expand Down Expand Up @@ -47,26 +58,18 @@ extension TreeSitterClient {
}
}

extension LanguageLayer.Content {
/// this should probably move into SwiftTreeSitterLayer
init(string: String, limit: Int) {
let read = Parser.readFunction(for: string, limit: limit)

self.init(
readHandler: read,
textProvider: string.predicateTextProvider
)
}
}

#if os(macOS) || os(iOS) || os(visionOS)
extension TextViewSystemInterface {
func languageLayerContent(with limit: Int) -> LanguageLayer.Content {
LanguageLayer.Content(string: textStorage.string, limit: limit)
}

func languageLayerContentSnapshot(with limit: Int) -> LanguageLayer.ContentSnapshot {
LanguageLayer.ContentSnapshot(string: textStorage.string, limit: limit)
}
}

@available(macOS 12.0, macCatalyst 15.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS 13.0, macCatalyst 16.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension TreeSitterClient {
/// Highlight an input string.
public static func highlight(
Expand All @@ -75,14 +78,14 @@ extension TreeSitterClient {
rootLanguageConfig: LanguageConfiguration,
languageProvider: @escaping LanguageLayer.LanguageProvider
) async throws -> AttributedString {
let content = LanguageLayer.Content(string: string)
let content = LanguageLayer.ContentSnapshot(string: string)
let length = string.utf16.count

let client = try TreeSitterClient(
rootLanguageConfig: rootLanguageConfig,
configuration: Configuration(
languageProvider: languageProvider,
contentProvider: { _ in content },
contentSnapshopProvider: { _ in content },
lengthProvider: { length },
invalidationHandler: { _ in },
locationTransformer: { _ in nil }
Expand Down
Loading

0 comments on commit 26db3fe

Please sign in to comment.