├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitattributes
├── .mailmap
├── .spi.yml
├── Sources
├── SwiftTreeSitter
│ ├── Documentation.docc
│ │ ├── Code
│ │ │ ├── using-essentials-2-1-example.swift
│ │ │ ├── using-essentials-2-2-example.swift
│ │ │ ├── using-queries-1-1-highlights.scm
│ │ │ ├── using-essentials-2-3-example.swift
│ │ │ ├── using-queries-1-2-highlights.scm
│ │ │ ├── using-queries-1-3-highlights.scm
│ │ │ ├── using-essentials-2-4-example.swift
│ │ │ ├── using-essentials-2-5-example.swift
│ │ │ ├── using-queries-2-1-example.swift
│ │ │ ├── using-essentials-2-6-example.swift
│ │ │ ├── using-essentials-3-1-example.swift
│ │ │ ├── using-essentials-4-1-example.swift
│ │ │ ├── using-essentials-1-1-package.swift
│ │ │ ├── using-essentials-3-2-example.swift
│ │ │ ├── using-essentials-1-2-package.swift
│ │ │ ├── using-essentials-4-2-example.swift
│ │ │ ├── using-queries-2-2-example.swift
│ │ │ ├── using-essentials-1-3-package.swift
│ │ │ ├── using-essentials-4-3-example.swift
│ │ │ ├── using-queries-2-3-example.swift
│ │ │ ├── using-queries-2-4-example.swift
│ │ │ ├── using-essentials-4-4-example.swift
│ │ │ ├── using-queries-2-5-example.swift
│ │ │ ├── using-queries-2-6-example.swift
│ │ │ ├── using-essentials-4-5-example.swift
│ │ │ ├── using-essentials-3-3-example.swift
│ │ │ ├── using-essentials-3-4-example.swift
│ │ │ ├── using-queries-2-7-example.swift
│ │ │ ├── using-essentials-3-5-example.swift
│ │ │ ├── using-queries-2-8-example.swift
│ │ │ ├── using-queries-2-9-example.swift
│ │ │ └── using-essentials-3-6-example.swift
│ │ ├── Resources
│ │ │ ├── cursor-output.png
│ │ │ └── resolved-output.png
│ │ ├── Tutorials
│ │ │ ├── Using-Tree-Sitter.tutorial
│ │ │ ├── Queries.tutorial
│ │ │ ├── Processing-Edits.tutorial
│ │ │ ├── Parsing.tutorial
│ │ │ └── Resolving-Queries.tutorial
│ │ ├── SwiftTreeSitter.md
│ │ └── New Languages.md
│ ├── SendableTypes.swift
│ ├── Bundle+Extensions.swift
│ ├── String+TextProvider.swift
│ ├── Encoding+Helpers.swift
│ ├── String+Data.swift
│ ├── ResolvingQueryMatchSequence.swift
│ ├── Point.swift
│ ├── TSRange.swift
│ ├── TreeCursor.swift
│ ├── Language.swift
│ ├── Input.swift
│ ├── InputEdit.swift
│ ├── ResolvingQueryCursor.swift
│ ├── Tree.swift
│ ├── QueryDefinitions.swift
│ ├── Parser.swift
│ ├── Node.swift
│ ├── LanguageConfiguration.swift
│ ├── Predicate.swift
│ └── Query.swift
└── SwiftTreeSitterLayer
│ ├── IndexSet+Range.swift
│ ├── Queryable.swift
│ ├── ParseState.swift
│ ├── TreeSitter+Extensions.swift
│ ├── LanguageLayerQueryCursor.swift
│ ├── Snapshots.swift
│ └── LanguageLayer.swift
├── Projects
├── SwiftTreeSitterExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── SwiftTreeSitterExampleApp.swift
│ ├── SwiftTreeSitterExample.entitlements
│ └── ContentView.swift
└── SwiftTreeSitterExample.xcodeproj
│ └── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── .gitignore
├── .gitmodules
├── .editorconfig
├── Package.resolved
├── Tests
├── SwiftTreeSitterTests
│ ├── LanguageConfigurationTests.swift
│ ├── ResolvingQueryCursorTests.swift
│ ├── NodeTests.swift
│ ├── QueryTests.swift
│ ├── ParserTests.swift
│ └── PredicateTests.swift
└── SwiftTreeSitterLayerTests
│ └── LanguageLayerTests.swift
├── LICENSE
├── Package.swift
├── CODE_OF_CONDUCT.md
├── .swiftpm
└── xcode
│ └── xcshareddata
│ └── xcschemes
│ └── SwiftTreeSitter.xcscheme
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [mattmassicotte]
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | tree-sitter/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Matt Massicotte <85322+mattmassicotte@users.noreply.github.com>
2 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [SwiftTreeSitter]
5 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-1-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-2-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "tree-sitter-swift"]
2 | path = tree-sitter-swift
3 | url = https://github.com/alex-pinkus/tree-sitter-swift
4 | branch = with-generated-files
5 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-1-1-highlights.scm:
--------------------------------------------------------------------------------
1 | ; SomeType.method(): highlight "SomeType" as a type
2 | (navigation_expression
3 | (simple_identifier) @type)
4 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-3-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-1-2-highlights.scm:
--------------------------------------------------------------------------------
1 | ; SomeType.method(): highlight "SomeType" as a type
2 | ((navigation_expression
3 | (simple_identifier) @type)
4 | )
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Resources/cursor-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tree-sitter/swift-tree-sitter/HEAD/Sources/SwiftTreeSitter/Documentation.docc/Resources/cursor-output.png
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitterLayer/IndexSet+Range.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension IndexSet {
4 | init(integersIn nsRange: NSRange) {
5 | self.init(integersIn: Range(nsRange) ?? 0..<0)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Resources/resolved-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tree-sitter/swift-tree-sitter/HEAD/Sources/SwiftTreeSitter/Documentation.docc/Resources/resolved-output.png
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-1-3-highlights.scm:
--------------------------------------------------------------------------------
1 | ; SomeType.method(): highlight "SomeType" as a type
2 | ((navigation_expression
3 | (simple_identifier) @type)
4 | (#match? @type "^[A-Z]"))
5 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-4-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/SwiftTreeSitterExampleApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct SwiftTreeSitterExampleApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/SendableTypes.swift:
--------------------------------------------------------------------------------
1 | struct SendableOpaquePointer: @unchecked Sendable {
2 | let pointer: OpaquePointer
3 |
4 | init(_ pointer: OpaquePointer) {
5 | self.pointer = pointer
6 | }
7 | }
8 |
9 | extension SendableOpaquePointer: Equatable {}
10 | extension SendableOpaquePointer: Hashable {}
11 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-5-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "tree-sitter",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/tree-sitter/tree-sitter",
7 | "state" : {
8 | "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406",
9 | "version" : "0.25.10"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-1-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/SwiftTreeSitterExample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-2-6-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-3-1-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-4-1-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
--------------------------------------------------------------------------------
/Tests/SwiftTreeSitterTests/LanguageConfigurationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftTreeSitter
4 | import TestTreeSitterSwift
5 |
6 | final class LanguageConfigurationTests: XCTestCase {
7 | func testCreateLanguageWithIncorrectBundleName() throws {
8 | let language = Language(language: tree_sitter_swift())
9 |
10 | XCTAssertThrowsError(try LanguageConfiguration(language, name: "Swift", bundleName: "abc"))
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Bundle+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if !os(WASI)
4 | extension Bundle {
5 | var isXCTestRunner: Bool {
6 | #if DEBUG
7 | return NSClassFromString("XCTest") != nil
8 | #else
9 | return false
10 | #endif
11 | }
12 |
13 | static var testBundle: Bundle? {
14 | return allBundles.first(where: {
15 | $0.bundlePath.components(separatedBy: "/").last?.contains("Tests.xctest") == true
16 | })
17 | }
18 | }
19 | #endif
20 |
21 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-1-1-package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TreeSitterExample",
7 | products: [
8 | .library(name: "TreeSitterExample", targets: ["TreeSitterExample"]),
9 | ],
10 | dependencies: [
11 | ],
12 | targets: [
13 | .target(name: "TreeSitterExample", dependencies: [
14 | ]),
15 | ]
16 | )
17 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-3-2-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let newSource = """
20 | func hello() {
21 | print("hello from SwiftTreeSitter")
22 | }
23 | """
24 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-1-2-package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TreeSitterExample",
7 | products: [
8 | .library(name: "TreeSitterExample", targets: ["TreeSitterExample"]),
9 | ],
10 | dependencies: [
11 | .package(url: "https://github.com/ChimeHQ/SwiftTreeSitter"),
12 | ],
13 | targets: [
14 | .target(name: "TreeSitterExample", dependencies: [
15 | "SwiftTreeSitter",
16 | ]),
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-4-2-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let url = Bundle.main
20 | .resourceURL?
21 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
22 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
23 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-2-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-1-3-package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TreeSitterExample",
7 | products: [
8 | .library(name: "TreeSitterExample", targets: ["TreeSitterExample"]),
9 | ],
10 | dependencies: [
11 | .package(url: "https://github.com/ChimeHQ/SwiftTreeSitter"),
12 | .package(url: "https://github.com/alex-pinkus/tree-sitter-swift", branch: "with-generated-files"),
13 | ],
14 | targets: [
15 | .target(name: "TreeSitterExample", dependencies: [
16 | "SwiftTreeSitter",
17 | "TreeSitterSwift",
18 | ]),
19 | ]
20 | )
21 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-4-3-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let url = Bundle.main
20 | .resourceURL?
21 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
22 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
23 |
24 | let query = try language.query(contentsOf: url!)
25 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-3-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
25 | let cursor = query.execute(node: tree.rootNode!)
26 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-4-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
25 | let cursor = query.execute(node: tree.rootNode!)
26 |
27 | let typeCaptures = cursor
28 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-4-4-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)
16 |
17 | print("tree: ", tree)
18 |
19 | let url = Bundle.main
20 | .resourceURL?
21 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
22 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
23 |
24 | let query = try language.query(contentsOf: url!)
25 |
26 | let cursor = query.execute(node: tree.rootNode!)
27 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "tree-sitter-markdown",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/MDeiml/tree-sitter-markdown",
7 | "state" : {
8 | "branch" : "split_parser",
9 | "revision" : "fa6bfd51727e4bef99f7eec5f43947f73d64ea7d"
10 | }
11 | },
12 | {
13 | "identity" : "tree-sitter-swift",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/alex-pinkus/tree-sitter-swift/",
16 | "state" : {
17 | "branch" : "with-generated-files",
18 | "revision" : "f04b305a7f18009c47091247d662707a8b629669"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-5-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
25 | let cursor = query.execute(node: tree.rootNode!)
26 |
27 | let typeCaptures = cursor
28 | .map { $0.captures(named: "type") }
29 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-6-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
25 | let cursor = query.execute(node: tree.rootNode!)
26 |
27 | let typeCaptures = cursor
28 | .map { $0.captures(named: "type") }
29 | .flatMap({ $0 })
30 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-4-5-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)
16 |
17 | print("tree: ", tree)
18 |
19 | let url = Bundle.main
20 | .resourceURL?
21 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
22 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
23 |
24 | let query = try language.query(contentsOf: url!)
25 |
26 | let cursor = query.execute(node: tree.rootNode!)
27 |
28 | for match in cursor {
29 | print("match: ", match)
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/String+TextProvider.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension String {
4 | /// Produces a `TextProvider` for use with `Predicate` resolution.
5 | @available(*, deprecated, renamed: "predicateTextProvider")
6 | public var cursorTextProvider: Predicate.TextProvider {
7 | return { (nsRange, _) in
8 | guard let range = Range(nsRange, in: self) else {
9 | return nil
10 | }
11 |
12 | return String(self[range])
13 | }
14 | }
15 |
16 | public var predicateTextProvider: Predicate.TextProvider {
17 | predicateTextSnapshotProvider
18 | }
19 |
20 | public var predicateTextSnapshotProvider: Predicate.TextSnapshotProvider {
21 | { (nsRange, _) in
22 | guard let range = Range(nsRange, in: self) else {
23 | return nil
24 | }
25 |
26 | return String(self[range])
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-3-3-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let newSource = """
20 | func hello() {
21 | print("hello from SwiftTreeSitter")
22 | }
23 | """
24 |
25 | let edit = InputEdit(startByte: 34,
26 | oldEndByte: 45,
27 | newEndByte: 49,
28 | startPoint: Point(row: 1, column: 22),
29 | oldEndPoint: Point(row: 1, column: 33),
30 | newEndPoint: Point(row: 1, column: 37))
31 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-3-4-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let newSource = """
20 | func hello() {
21 | print("hello from SwiftTreeSitter")
22 | }
23 | """
24 |
25 | let edit = InputEdit(startByte: 34,
26 | oldEndByte: 45,
27 | newEndByte: 49,
28 | startPoint: Point(row: 1, column: 22),
29 | oldEndPoint: Point(row: 1, column: 33),
30 | newEndPoint: Point(row: 1, column: 37))
31 |
32 | tree.edit(edit)
33 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Tutorials/Using-Tree-Sitter.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorials(name: "Using SwiftTreeSitter") {
2 | @Intro(title: "Using SwiftTreeSitter") {
3 | SwiftTreeSitter provides a Swift interface to the tree-sitter incremental parsing system. You can use it to parse language text, reparse it as it changes, and query the syntax tree.
4 | }
5 |
6 | @Chapter(name: "Tree-Sitter Essentials") {
7 | Learn how to use parse text, process edits, and perform queries with tree-sitter.
8 |
9 | @TutorialReference(tutorial: "doc:Parsing")
10 | @TutorialReference(tutorial: "doc:Processing-Edits")
11 | @TutorialReference(tutorial: "doc:Queries")
12 | }
13 |
14 | @Chapter(name: "Doing more with Queries") {
15 | Learn how to take full advantage of the tree-sitter query system.
16 |
17 | @TutorialReference(tutorial: "doc:Resolving-Queries")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-queries-2-7-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func example() {
11 | SomeType.method()
12 | variable.method()
13 | }
14 | """
15 |
16 | let tree = parser.parse(source)!
17 |
18 | let url = Bundle.main
19 | .resourceURL?
20 | .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
21 | .appendingPathComponent("Contents/Resources/queries/highlights.scm")
22 |
23 | let query = try language.query(contentsOf: url!)
24 |
25 | let cursor = query.execute(node: tree.rootNode!)
26 |
27 | let typeCaptures = cursor
28 | .map { $0.captures(named: "type") }
29 | .flatMap({ $0 })
30 |
31 | for capture in typeCaptures {
32 | print("matched range:", capture.range)
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Code/using-essentials-3-5-example.swift:
--------------------------------------------------------------------------------
1 | import SwiftTreeSitter
2 | import TreeSitterSwift
3 |
4 | let language = Language(language: tree_sitter_swift())
5 |
6 | let parser = Parser()
7 | try parser.setLanguage(language)
8 |
9 | let source = """
10 | func hello() {
11 | print("hello from tree-sitter")
12 | }
13 | """
14 |
15 | let tree = parser.parse(source)!
16 |
17 | print("tree: ", tree)
18 |
19 | let newSource = """
20 | func hello() {
21 | print("hello from SwiftTreeSitter")
22 | }
23 | """
24 |
25 | let edit = InputEdit(startByte: 34,
26 | oldEndByte: 45,
27 | newEndByte: 49,
28 | startPoint: Point(row: 1, column: 22),
29 | oldEndPoint: Point(row: 1, column: 33),
30 | newEndPoint: Point(row: 1, column: 37))
31 |
32 | tree.edit(edit)
33 |
34 | let newTree = parser.parse(tree, string: newSource)
35 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Encoding+Helpers.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | extension String.Encoding {
5 | var internalEncoding: TSInputEncoding? {
6 | switch self {
7 | case .utf8:
8 | return TSInputEncodingUTF8
9 | case .utf16LittleEndian:
10 | return TSInputEncodingUTF16LE
11 | case .utf16BigEndian:
12 | return TSInputEncodingUTF16BE
13 | default:
14 | return nil
15 | }
16 | }
17 | }
18 |
19 | public extension NSRange {
20 | var byteRange: Range {
21 | let lower = UInt32(location * 2)
22 | let upper = UInt32(NSMaxRange(self) * 2)
23 |
24 | return lower.. Data? {
13 | precondition(encoding.internalEncoding != nil)
14 |
15 | let location = byteOffset / 2
16 |
17 | let end = min(location + (chunkSize / 2), limit)
18 |
19 | if location > end {
20 | assertionFailure("location is greater than end")
21 | return nil
22 | }
23 |
24 | let range = NSRange(location..
16 | -
17 | - ``Parser``
18 | - ``Language``
19 | - ``InputEdit``
20 |
21 | ### Trees
22 |
23 | - ``Tree``
24 | - ``Node``
25 | - ``TreeCursor``
26 |
27 | ### Queries
28 |
29 | - ``Query``
30 | - ``QueryCursor``
31 | - ``ResolvingQueryMatchSequence``
32 | - ``QueryCapture``
33 | - ``QueryMatch``
34 | - ``QueryError``
35 | - ``Predicate``
36 | - ``QueryPredicateError``
37 | - ``QueryPredicateStep``
38 |
39 | ### Structures
40 |
41 | - ``TSRange``
42 | - ``Point``
43 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitterLayer/Queryable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import SwiftTreeSitter
4 |
5 | public protocol Queryable {
6 | associatedtype Cursor : Sequence
7 | associatedtype Region
8 |
9 | func executeQuery(_ queryDef: Query.Definition, in region: Region) throws -> Cursor
10 | }
11 |
12 | extension Queryable {
13 | public func highlights(in region: Region, provider: SwiftTreeSitter.Predicate.TextProvider) throws -> [NamedRange] {
14 | try withoutActuallyEscaping(provider) { escapingClosure in
15 | try executeQuery(.highlights, in: region)
16 | .resolve(with: .init(textProvider: escapingClosure))
17 | .highlights()
18 | }
19 | }
20 | }
21 |
22 | extension Queryable where Region == IndexSet {
23 | public func executeQuery(_ queryDef: Query.Definition, in range: NSRange) throws -> Cursor {
24 | try executeQuery(queryDef, in: IndexSet(integersIn: range))
25 | }
26 |
27 | public func highlights(in range: NSRange, provider: SwiftTreeSitter.Predicate.TextProvider) throws -> [NamedRange] {
28 | try highlights(in: IndexSet(integersIn: range), provider: provider)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/ResolvingQueryMatchSequence.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct ResolvingQueryMatchSequence where MatchSequence.Element == QueryMatch {
4 | private let sequence: MatchSequence
5 | private var iterator: MatchSequence.Iterator
6 | private let context: Predicate.Context
7 |
8 | public init(sequence: MatchSequence, context: Predicate.Context) {
9 | self.sequence = sequence
10 | self.iterator = sequence.makeIterator()
11 | self.context = context.cachingContext
12 | }
13 |
14 | /// Interpret the sequence using the "injections.scm" definition
15 | public func injections() -> [NamedRange] {
16 | return compactMap({ $0.injection(with: context.textProvider) })
17 | }
18 | }
19 |
20 | extension ResolvingQueryMatchSequence: Sequence, IteratorProtocol {
21 | public mutating func next() -> QueryMatch? {
22 | while let match = iterator.next() {
23 | if match.allowed(in: context) == false {
24 | continue
25 | }
26 |
27 | return match
28 | }
29 |
30 | return nil
31 | }
32 | }
33 |
34 | extension Sequence where Element == QueryMatch {
35 | public func resolve(with context: Predicate.Context) -> ResolvingQueryMatchSequence {
36 | ResolvingQueryMatchSequence(sequence: self, context: context)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitterLayer/ParseState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import SwiftTreeSitter
4 |
5 | struct ParseState {
6 | let tree: MutableTree?
7 | let includedSet: IndexSet?
8 |
9 | init(tree: MutableTree? = nil) {
10 | self.tree = tree
11 | self.includedSet = tree?.includedSet
12 | }
13 |
14 | func node(in range: Range) -> Node? {
15 | guard let root = tree?.rootNode else {
16 | return nil
17 | }
18 |
19 | return root.descendant(in: range)
20 | }
21 |
22 | func applyEdit(_ edit: InputEdit) {
23 | tree?.edit(edit)
24 | }
25 |
26 | func copy() -> ParseState {
27 | return ParseState(tree: tree?.mutableCopy())
28 | }
29 | }
30 |
31 | extension ParseState {
32 | func changedByteRanges(for otherState: ParseState) -> [Range] {
33 | let otherTree = otherState.tree
34 |
35 | switch (tree, otherTree) {
36 | case (let t1?, let t2?):
37 | return t1.changedRanges(from: t2).map({ $0.bytes })
38 | case (nil, let t2?):
39 | let range = t2.rootNode?.byteRange
40 |
41 | return range.flatMap({ [$0] }) ?? []
42 | case (_, nil):
43 | return []
44 | }
45 | }
46 |
47 | func changedSet(for otherState: ParseState) -> IndexSet {
48 | let ranges = changedByteRanges(for: otherState)
49 | .compactMap({ Range($0.range) })
50 |
51 | var set = IndexSet()
52 |
53 | for range in ranges {
54 | set.insert(integersIn: range)
55 | }
56 |
57 | return set
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Point.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Point.swift
3 | // SwiftTreeSitter
4 | //
5 | // Created by Matt Massicotte on 2018-12-18.
6 | // Copyright © 2018 Chime Systems. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TreeSitter
11 |
12 | public struct Point: Codable, Sendable {
13 | public let row: UInt32
14 | public let column: UInt32
15 |
16 | public init(row: UInt32, column: UInt32) {
17 | self.row = row
18 | self.column = column
19 | }
20 |
21 | public init(row: Int, column: Int) {
22 | self.row = UInt32(row)
23 | self.column = UInt32(column)
24 | }
25 |
26 | init(internalPoint: TSPoint) {
27 | self.row = internalPoint.row
28 | self.column = internalPoint.column
29 | }
30 |
31 | var internalPoint: TSPoint {
32 | return TSPoint(row: row, column: column)
33 | }
34 | }
35 |
36 | extension Point: Comparable {
37 | public static func < (lhs: Point, rhs: Point) -> Bool {
38 | if lhs.row < rhs.row {
39 | return true
40 | } else if lhs.row > rhs.row {
41 | return false
42 | } else {
43 | return lhs.column < rhs.column
44 | }
45 | }
46 | }
47 |
48 | extension Point: CustomStringConvertible {
49 | public var description: String {
50 | return "{\(row), \(column)}"
51 | }
52 | }
53 | extension Point: Hashable {
54 | }
55 |
56 | extension Point {
57 | public static let zero = Point(row: 0, column: 0)
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/SwiftTreeSitterTests/ResolvingQueryCursorTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftTreeSitter
4 | import TestTreeSitterSwift
5 |
6 | @available(*, deprecated)
7 | final class ResolvingQueryCursorTests: XCTestCase {
8 | #if !os(WASI)
9 | private static let swiftLang = Language(language: tree_sitter_swift())
10 |
11 | func testIsNotPredicate() throws {
12 | let language = Self.swiftLang
13 | let queryText = """
14 | ("func" @keyword.function (#is-not? group))
15 | """
16 | let queryData = try XCTUnwrap(queryText.data(using: .utf8))
17 | let query = try Query(language: language, data: queryData)
18 |
19 | let text = """
20 | func a() {}
21 | func b() {}
22 | """
23 |
24 | let parser = Parser()
25 | try parser.setLanguage(language)
26 |
27 | let tree = try XCTUnwrap(parser.parse(text))
28 | let root = try XCTUnwrap(tree.rootNode)
29 |
30 | let context = Predicate.Context(textProvider: { _, _ in return nil },
31 | groupMembershipProvider: {name, range, _ in
32 | XCTAssertEqual(name, "group")
33 |
34 | return range == NSRange(12..<16)
35 | })
36 |
37 | let cursor = query.execute(node: root, in: tree)
38 | let resolvingCursor = ResolvingQueryCursor(cursor: cursor, context: context)
39 |
40 | let expected = [
41 | NamedRange(name: "keyword.function", range: NSRange(0..<4), pointRange: Point(row: 0, column: 0)..
6 | public let bytes: Range
7 |
8 | public init(points: Range, bytes: Range) {
9 | self.points = points
10 | self.bytes = bytes
11 | }
12 |
13 | init(internalRange range: TreeSitter.TSRange) {
14 | self.bytes = range.start_byte.. Bool {
38 | return lhs.points.lowerBound < rhs.points.lowerBound
39 | }
40 | }
41 |
42 | extension TSRange: CustomDebugStringConvertible {
43 | public var debugDescription: String {
44 | ""
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2021, Chime
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let settings: [SwiftSetting] = [
6 | .enableExperimentalFeature("StrictConcurrency")
7 | ]
8 |
9 | let package = Package(
10 | name: "SwiftTreeSitter",
11 | platforms: [
12 | .macOS(.v10_13),
13 | .macCatalyst(.v13),
14 | .iOS(.v12),
15 | .tvOS(.v12),
16 | .watchOS(.v5),
17 | .visionOS(.v1),
18 | ],
19 | products: [
20 | .library(name: "SwiftTreeSitter", targets: ["SwiftTreeSitter"]),
21 | .library(name: "SwiftTreeSitterLayer", targets: ["SwiftTreeSitterLayer"]),
22 | ],
23 | dependencies: [
24 | .package(url: "https://github.com/tree-sitter/tree-sitter", .upToNextMinor(from: "0.25.0"))
25 | ],
26 | targets: [
27 | .target(
28 | name: "TestTreeSitterSwift",
29 | path: "tree-sitter-swift",
30 | sources: ["src/parser.c", "src/scanner.c"],
31 | publicHeadersPath: "bindings/swift",
32 | cSettings: [.headerSearchPath("src")]
33 | ),
34 | .target(
35 | name: "SwiftTreeSitter",
36 | dependencies: [
37 | .product(name: "TreeSitter", package: "tree-sitter")
38 | ],
39 | swiftSettings: settings
40 | ),
41 | .testTarget(
42 | name: "SwiftTreeSitterTests",
43 | dependencies: ["SwiftTreeSitter", "TestTreeSitterSwift"],
44 | swiftSettings: settings
45 | ),
46 | .target(
47 | name: "SwiftTreeSitterLayer",
48 | dependencies: ["SwiftTreeSitter"],
49 | swiftSettings: settings
50 | ),
51 | .testTarget(
52 | name: "SwiftTreeSitterLayerTests",
53 | dependencies: ["SwiftTreeSitterLayer", "TestTreeSitterSwift"],
54 | swiftSettings: settings
55 | ),
56 | ]
57 | )
58 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - 'README.md'
9 | - '**/*.docc/**'
10 | - 'CODE_OF_CONDUCT.md'
11 | - '.editorconfig'
12 | - '.spi.yml'
13 | pull_request:
14 | branches:
15 | - main
16 |
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | test:
23 | name: Test
24 | timeout-minutes: 30
25 | runs-on: macOS-15
26 | env:
27 | DEVELOPER_DIR: /Applications/Xcode_16.4.app
28 | strategy:
29 | matrix:
30 | destination:
31 | - "platform=macOS"
32 | - "platform=macOS,variant=Mac Catalyst"
33 | - "platform=iOS Simulator,name=iPhone 16"
34 | - "platform=tvOS Simulator,name=Apple TV"
35 | - "platform=watchOS Simulator,name=Apple Watch Series 10 (42mm)"
36 | - "platform=visionOS Simulator,name=Apple Vision Pro,OS=2.5"
37 | steps:
38 | - name: Checkout
39 | uses: actions/checkout@v4
40 | with:
41 | submodules: recursive
42 | - name: Test platform ${{ matrix.destination }}
43 | run: set -o pipefail && xcodebuild -scheme SwiftTreeSitter -destination "${{ matrix.destination }}" test | xcbeautify
44 |
45 | linux_test:
46 | name: Test Linux
47 | runs-on: ubuntu-latest
48 | timeout-minutes: 30
49 | steps:
50 | - name: Checkout
51 | uses: actions/checkout@v4
52 | with:
53 | submodules: recursive
54 | - name: Swiftly
55 | uses: vapor/swiftly-action@v0.2.0
56 | with:
57 | toolchain: 6.1.0
58 | - name: Test
59 | run: swift test
60 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Tutorials/Queries.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 10) {
2 | @Intro(title: "Running Queries") {
3 | Building and maintaining a parse tree on its own isn't very useful. You'll probably want to inspect the tree using language-specific patterns and matches. The tree-sitter query system is built to do exactly that.
4 | }
5 |
6 | @Section(title: "Getting Query Definitions") {
7 | @ContentAndMedia {
8 | Let's build on our original example by running queries against our tree. You can write your own queries. Here, we'll use `highlights.scm`, a file many parser include for syntax highlighting.
9 | }
10 |
11 | @Steps {
12 | @Step {
13 | Here we've parsed some text and have tree object set up.
14 |
15 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-4-1-example.swift")
16 | }
17 |
18 | @Step {
19 | Create a `URL` to the query definition file.
20 |
21 | These are copied into the Swift packages. Their locations differ for macOS and iOS.
22 |
23 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-4-2-example.swift")
24 | }
25 |
26 | @Step {
27 | Initialize the `query` object.
28 |
29 | This can be expensive, depending on the language grammar/queries.
30 |
31 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-4-3-example.swift")
32 | }
33 |
34 | @Step {
35 | Execute the query.
36 |
37 | Queries must be run against a tree
38 | This produces a `QueryCursor`, which can be used to iterate over the results.
39 |
40 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-4-4-example.swift")
41 | }
42 |
43 | @Step {
44 | Use a loop to print all of the matches.
45 |
46 | `QueryCursor` conforms to `Sequence`.
47 |
48 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-4-5-example.swift")
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/New Languages.md:
--------------------------------------------------------------------------------
1 | # Adding Language Parsers
2 |
3 | Parsers are separate projects that are required to work with a language.
4 |
5 | ## Overview
6 |
7 | SwiftTreeSitter is largely a wrapper around the tree-sitter runtime API. On its own, it cannot parse anything. The runtime must be combined with a parser project made for a specific language grammar. If you are interested in using tree-sitter, you'll probably need at least one parser.
8 |
9 | They can be built manually or integrated with SPM.
10 |
11 | > Important: The tree-sitter system is used by many other projects. Please commit parsers improvements back to their main repositories.
12 |
13 | ### Using SPM
14 |
15 | If you're using Swift, you'll probably be most interested in using SPM for your parser dependencies. This is probably the most convenient option, but may require adding SPM support to the parser. If you are adding SPM support to a language, we can list your temporary fork in the README.
16 |
17 | For an example of the changes needed, see [this PR](https://github.com/tree-sitter/tree-sitter-java/pull/113).
18 |
19 | ## Building Manually
20 |
21 | One option is just to build parser manually. This is typically done with node. Interfacing the resulting outputs with Swift will typically involve writing a custom C header file, using `ar` to build a static library, and then laying everything out with a `Module.modulemap` file. It's laborious, but possible.
22 |
23 | ### Using Make
24 |
25 | I was so frustrated with the manual build process, that I began [adapting](https://github.com/tree-sitter/tree-sitter/issues/1488) the runtime's Makefile to the parsers. Only a handful are done. But, if you want to proceed with a custom build, I would recommend taking the time to do this.
26 |
27 | The process is now dialed in enough that nearly all aspects of the Make system are parser-generic. You really just need to drop in some files and open a pull request on the main parser repository.
28 |
29 | For an example of the changes needed, see [this PR](https://github.com/tree-sitter/tree-sitter-java/pull/110).
30 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/TreeCursor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | public class TreeCursor {
5 | private var internalCursor: TSTreeCursor
6 | private let internalTree: Tree
7 |
8 | init(internalCursor: TSTreeCursor, internalTree: Tree) {
9 | self.internalCursor = internalCursor
10 | self.internalTree = internalTree
11 | }
12 |
13 | deinit {
14 | ts_tree_cursor_delete(&internalCursor)
15 | }
16 | }
17 |
18 | extension TreeCursor {
19 | public func gotoParent() -> Bool {
20 | return ts_tree_cursor_goto_parent(&internalCursor)
21 | }
22 |
23 | public func gotoNextSibling() -> Bool {
24 | return ts_tree_cursor_goto_next_sibling(&internalCursor)
25 | }
26 |
27 | public func goToFirstChild() -> Bool {
28 | return ts_tree_cursor_goto_first_child(&internalCursor)
29 | }
30 |
31 | public func goToFirstChild(for startByte: UInt32) -> Bool {
32 | return ts_tree_cursor_goto_first_child_for_byte(&internalCursor, startByte) != -1
33 | }
34 | }
35 |
36 | extension TreeCursor {
37 | public var currentNode: Node? {
38 | return Node(internalNode: ts_tree_cursor_current_node(&internalCursor), internalTree: internalTree)
39 | }
40 |
41 | public var currentFieldName: String? {
42 | guard let str = ts_tree_cursor_current_field_name(&internalCursor) else {
43 | return nil
44 | }
45 |
46 | return String(cString: str)
47 | }
48 |
49 | public var currentFieldId: Int {
50 | return Int(ts_tree_cursor_current_field_id(&internalCursor))
51 | }
52 | }
53 |
54 | extension TreeCursor {
55 | public func enumerateCurrentAndDescendents(block: (Node) throws -> Void) rethrows {
56 | if let node = currentNode {
57 | try block(node)
58 | }
59 |
60 | if goToFirstChild() == false {
61 | return
62 | }
63 |
64 | try enumerateCurrentAndDescendents(block: block)
65 |
66 | while gotoNextSibling() {
67 | try enumerateCurrentAndDescendents(block: block)
68 | }
69 |
70 | let success = gotoParent()
71 |
72 | assert(success)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Language.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | public struct Language: Sendable {
5 | private let tsLanguagePointer: SendableOpaquePointer
6 |
7 | /// Creates an instance.
8 | ///
9 | /// - Parameters:
10 | /// - language: The TSLanguage instance to wrap.
11 | public init(language: OpaquePointer) {
12 | self.init(language)
13 | }
14 |
15 | /// Creates a new instance by wrapping a pointer to a tree sitter parser.
16 | ///
17 | /// - Parameters:
18 | /// - language: The TSLanguage instance to wrap.
19 | public init(_ language: OpaquePointer) {
20 | self.tsLanguagePointer = SendableOpaquePointer(language)
21 | }
22 |
23 | public var tsLanguage: OpaquePointer {
24 | tsLanguagePointer.pointer
25 | }
26 | }
27 |
28 | extension Language {
29 | public static let version = Int(TREE_SITTER_LANGUAGE_VERSION)
30 | public static let minimumCompatibleVersion = Int(TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION)
31 |
32 | public var ABIVersion: Int {
33 | return Int(ts_language_version(tsLanguage))
34 | }
35 |
36 | public var fieldCount: Int {
37 | return Int(ts_language_field_count(tsLanguage))
38 | }
39 |
40 | public var symbolCount: Int {
41 | return Int(ts_language_symbol_count(tsLanguage))
42 | }
43 |
44 | public func fieldName(for id: Int) -> String? {
45 | guard let str = ts_language_field_name_for_id(tsLanguage, TSFieldId(id)) else { return nil }
46 |
47 | return String(cString: str)
48 | }
49 |
50 | public func fieldId(for name: String) -> Int? {
51 | let count = UInt32(name.utf8.count)
52 |
53 | let value = name.withCString { cStr in
54 | return ts_language_field_id_for_name(tsLanguage, cStr, count)
55 | }
56 |
57 | return Int(value)
58 | }
59 |
60 | public func symbolName(for id: Int) -> String? {
61 | guard let str = ts_language_symbol_name(tsLanguage, TSSymbol(id)) else {
62 | return nil
63 | }
64 |
65 | return String(cString: str)
66 | }
67 | }
68 |
69 | extension Language: Hashable {
70 | }
71 |
72 | extension Language {
73 | /// Construct a query object from data in a file.
74 | public func query(contentsOf url: URL) throws -> Query {
75 | let data = try Data(contentsOf: url)
76 |
77 | return try Query(language: self, data: data)
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Tutorials/Processing-Edits.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 10) {
2 | @Intro(title: "Processing Edits") {
3 | Tree-sitter can be used to process static text. But, its real power is its ability to handle changes.
4 |
5 | With basic parsing down, let's make a change a look at how incremental re-parsing is done.
6 | }
7 |
8 | @Section(title: "Create an InputEdit and Update an Existing Tree") {
9 | @ContentAndMedia {
10 | Starting with our previous example, we now have a parsed tree. We can now supply some new text, compute the needed edit values, and re-parse the content. This will allow us to compute what's been changed in the source.
11 | }
12 |
13 | @Steps {
14 | @Step {
15 | Change handling must start with an existing `Tree` structure.
16 |
17 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-1-example.swift")
18 | }
19 |
20 | @Step {
21 | Create some new text.
22 |
23 | Note the "edit" we've done is changing the content of the string.
24 |
25 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-2-example.swift")
26 | }
27 |
28 | @Step {
29 | Calculate the `InputEdit` structure that describes the change.
30 |
31 | This involves computing UTF-16 byte offsets and `Point` values.
32 |
33 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-3-example.swift")
34 | }
35 |
36 | @Step {
37 | Apply the edit to the existing `tree` object.
38 |
39 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-4-example.swift")
40 | }
41 |
42 | @Step {
43 | Re-parse the edited tree, supplying the new text.
44 |
45 | Tree-sitter will use the updated tree and grammar rules to re-read the minimum amount of text required to form a valid syntax tree.
46 |
47 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-5-example.swift")
48 | }
49 |
50 | @Step {
51 | Calculate the differences.
52 |
53 | These ranges will tell you which elements of the text were affected by the edit.
54 |
55 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-3-6-example.swift")
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitterLayer/TreeSitter+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftTreeSitter
3 |
4 | extension Point {
5 | public typealias LocationTransformer = (Int) -> Point?
6 | }
7 |
8 | extension InputEdit {
9 | init?(range: NSRange, delta: Int, oldEndPoint: Point, transformer: Point.LocationTransformer? = nil) {
10 | let startLocation = range.location
11 | let newEndLocation = range.upperBound + delta
12 |
13 | if newEndLocation < 0 {
14 | assertionFailure("invalid range/delta")
15 | return nil
16 | }
17 |
18 | let startPoint = transformer?(startLocation)
19 | let newEndPoint = transformer?(newEndLocation)
20 |
21 | if transformer != nil {
22 | assert(startPoint != nil)
23 | assert(newEndPoint != nil)
24 | }
25 |
26 | self.init(startByte: UInt32(range.location * 2),
27 | oldEndByte: UInt32(range.upperBound * 2),
28 | newEndByte: UInt32(newEndLocation * 2),
29 | startPoint: startPoint ?? .zero,
30 | oldEndPoint: oldEndPoint,
31 | newEndPoint: newEndPoint ?? .zero)
32 | }
33 |
34 | init(range: NSRange, delta: Int, oldEndPoint: Point, transformer: Point.LocationTransformer) {
35 | let startLocation = range.location
36 | let newEndLocation = range.upperBound + delta
37 |
38 | assert(startLocation >= 0)
39 | assert(newEndLocation >= startLocation)
40 |
41 | let startPoint = transformer(startLocation) ?? .zero
42 | let newEndPoint = transformer(newEndLocation) ?? .zero
43 |
44 | assert(oldEndPoint >= startPoint)
45 | assert(newEndPoint >= startPoint)
46 |
47 | self.init(startByte: UInt32(range.location * 2),
48 | oldEndByte: UInt32(range.upperBound * 2),
49 | newEndByte: UInt32(newEndLocation * 2),
50 | startPoint: startPoint,
51 | oldEndPoint: oldEndPoint,
52 | newEndPoint: newEndPoint)
53 | }
54 | }
55 |
56 | extension Parser {
57 | func parse(state: ParseState, readHandler: Parser.ReadBlock) -> ParseState {
58 | let newTree = parse(tree: state.tree, readBlock: readHandler)
59 |
60 | return ParseState(tree: newTree)
61 | }
62 | }
63 |
64 | extension MutableTree {
65 | var includedSet: IndexSet {
66 | var set = IndexSet()
67 |
68 | for tsRange in includedRanges {
69 | guard let range = Range(tsRange.bytes.range) else { continue }
70 |
71 | set.insert(integersIn: range)
72 | }
73 |
74 | return set
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Input.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Input.swift
3 | // SwiftTreeSitter
4 | //
5 | // Created by Matt Massicotte on 2018-12-18.
6 | // Copyright © 2018 Chime Systems. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TreeSitter
11 |
12 | final class Input {
13 | typealias Buffer = UnsafeMutableBufferPointer
14 |
15 | private let encoding: TSInputEncoding
16 | fileprivate let readBlock: Parser.ReadBlock
17 | private var internalBuffer: Buffer?
18 |
19 | init(encoding: TSInputEncoding, readBlock: @escaping Parser.ReadBlock) {
20 | self.encoding = encoding
21 | self.readBlock = readBlock
22 | }
23 |
24 | deinit {
25 | buffer = nil
26 | }
27 |
28 | fileprivate var buffer: Buffer? {
29 | get {
30 | return internalBuffer
31 | }
32 | set {
33 | internalBuffer?.deallocate()
34 | internalBuffer = newValue
35 | }
36 | }
37 |
38 | fileprivate var bufferPointer: UnsafePointer? {
39 | return buffer.flatMap { UnsafePointer($0.baseAddress) }
40 | }
41 |
42 | var internalInput: TSInput? {
43 | let unmanaged = Unmanaged.passUnretained(self)
44 |
45 | return TSInput(payload: unmanaged.toOpaque(), read: readFunction, encoding: encoding, decode: nil)
46 | }
47 | }
48 |
49 | private func readFunction(
50 | payload: UnsafeMutableRawPointer?,
51 | byteIndex: UInt32,
52 | position: TSPoint,
53 | bytesRead: UnsafeMutablePointer?
54 | ) -> UnsafePointer? {
55 | // get our self reference
56 | let wrapper: Input = Unmanaged.fromOpaque(payload!).takeUnretainedValue()
57 |
58 | // call our Swift-friendly reader block, or early out if there's no data to copy.
59 | guard
60 | let data = wrapper.readBlock(Int(byteIndex), Point(internalPoint: position)),
61 | data.count > 0
62 | else
63 | {
64 | bytesRead?.pointee = 0
65 | return nil
66 | }
67 |
68 | // copy the data into an internally-managed buffer with a lifetime of wrapper
69 | let buffer = Input.Buffer.allocate(capacity: data.count)
70 | let copiedLength = data.copyBytes(to: buffer)
71 | precondition(copiedLength == data.count)
72 |
73 | wrapper.buffer = buffer
74 |
75 | // return to the caller
76 | bytesRead?.pointee = UInt32(buffer.count)
77 |
78 | return wrapper.bufferPointer
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/SwiftTreeSitterTests/NodeTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftTreeSitter
4 | import TestTreeSitterSwift
5 |
6 | final class NodeTests: XCTestCase {
7 | #if !os(WASI)
8 | func testTreeNodeLifecycle() throws {
9 | let language = Language(language: tree_sitter_swift())
10 |
11 | let text = """
12 | func main() {
13 | }
14 | """
15 |
16 | let parser = Parser()
17 | try parser.setLanguage(language)
18 |
19 | var tree: MutableTree? = try XCTUnwrap(parser.parse(text))
20 | let root = try XCTUnwrap(tree?.rootNode)
21 |
22 | tree = nil
23 |
24 | XCTAssertTrue(root.childCount != 0)
25 | }
26 | #endif
27 |
28 | func testTreeNodeText() throws {
29 | let language = Language(language: tree_sitter_swift())
30 |
31 | let text = """
32 | func greet(name: String){
33 | print("hello,\(name)")
34 | }
35 | greet("world")
36 | """
37 |
38 | let parser = Parser()
39 | try parser.setLanguage(language)
40 |
41 | let tree: MutableTree? = try XCTUnwrap(parser.parse(text))
42 | let root = try XCTUnwrap(tree?.rootNode)
43 |
44 | // function_declaration
45 | let function_declaration_node = try XCTUnwrap(root.child(at: 0))
46 |
47 | for i in (0.. Element? {
46 | while activeCursor != nil {
47 | if let match = activeCursor?.next() {
48 | return match
49 | }
50 |
51 | // our match has returned nil, do we need to advance to the next range?
52 | self.advanceRange()
53 | }
54 |
55 | return nil
56 | }
57 | }
58 |
59 | public struct LanguageTreeQueryCursor {
60 | private var activeCursor: LanguageLayerQueryCursor?
61 | private let targets: [LanguageLayerQueryCursor.Target]
62 | private var index: Int
63 | private var set: IndexSet
64 |
65 | init(set: IndexSet, targets: [LanguageLayerQueryCursor.Target]) {
66 | self.set = set
67 | self.targets = targets
68 | self.index = targets.index(before: targets.startIndex)
69 |
70 | advanceCursor()
71 | }
72 | }
73 |
74 | extension LanguageTreeQueryCursor: Sequence, IteratorProtocol {
75 | public typealias Element = QueryMatch
76 |
77 | private mutating func advanceCursor() {
78 | self.index += 1
79 | guard index < targets.endIndex else {
80 | self.activeCursor = nil
81 | return
82 | }
83 |
84 | self.activeCursor = LanguageLayerQueryCursor(target: targets[index], set: set)
85 | }
86 |
87 | private mutating func expandSet(_ range: NSRange?) {
88 | if let range = range {
89 | self.set.formUnion(IndexSet(integersIn: range))
90 | }
91 | }
92 |
93 | public mutating func next() -> Element? {
94 | while activeCursor != nil {
95 | if let match = activeCursor?.next() {
96 | // matches can occur outside of our target and can affect sublayer queries
97 | expandSet(match.range)
98 |
99 | return match
100 | }
101 |
102 | // our match has returned nil, do we need to advance to the next cursor?
103 | self.advanceCursor()
104 | }
105 |
106 | return nil
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Tutorials/Parsing.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 10) {
2 | @Intro(title: "Parsing") {
3 | Tree-sitter is all about parsing. Learn how to set up and use a language parser to generate a syntax tree.
4 | }
5 |
6 | @Section(title: "Adding Dependencies") {
7 | @ContentAndMedia {
8 | This example will use both SwiftTreeSitter and the parser for the Swift language itself. SPM will be used, but similar steps can be done from within Xcode.
9 | }
10 |
11 | @Steps {
12 | @Step {
13 | Create a package.
14 |
15 | @Code(name: "Package.swift", file: "using-essentials-1-1-package.swift")
16 | }
17 |
18 | @Step {
19 | Add the `SwiftTreeSitter` dependency.
20 |
21 | @Code(name: "Package.swift", file: "using-essentials-1-2-package.swift")
22 | }
23 |
24 | @Step {
25 | Add the `TreeSitterSwift` dependency, using the branch with SPM support.
26 |
27 | We're going to be parsing Swift code here, and the module naming is confusing. The convention for parser modules is `TreeSitter` + `NameOfLanguage`.
28 |
29 |
30 | @Code(name: "Package.swift", file: "using-essentials-1-3-package.swift")
31 | }
32 | }
33 | }
34 |
35 | @Section(title: "Creating a Parser") {
36 | @ContentAndMedia {
37 | The core component of tree-sitter is the `Parser` object. Let's make one and use it to generate a tree representation of some Swift code.
38 | }
39 |
40 | @Steps {
41 | @Step {
42 | Import the runtime module.
43 |
44 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-1-example.swift")
45 | }
46 |
47 | @Step {
48 | Import the language parser
49 |
50 | Remember the names are close. This is the parser for the Swift language.
51 |
52 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-2-example.swift")
53 | }
54 |
55 | @Step {
56 | Create a `Language` object.
57 |
58 | Note the call to `tree_sitter_swift`. That function is defined in the Swift language module.
59 |
60 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-3-example.swift")
61 | }
62 |
63 | @Step {
64 | Create a `Parser` object and assign its language grammar.
65 |
66 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-4-example.swift")
67 | }
68 |
69 | @Step {
70 | Define a small amount of Swift code as a string.
71 |
72 | We'll use this as input to the parser.
73 |
74 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-5-example.swift")
75 | }
76 |
77 | @Step {
78 | Parse the text and print the result.
79 |
80 | @Code(name: "TreeSitterExample.swift", file: "using-essentials-2-6-example.swift")
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitterLayer/Snapshots.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import SwiftTreeSitter
4 |
5 | public struct LanguageLayerSnapshot: Sendable {
6 | public private(set) var tree: Tree
7 | public let data: LanguageData
8 | public let depth: Int
9 |
10 | public init(tree: Tree, data: LanguageData, depth: Int) {
11 | self.tree = tree
12 | self.data = data
13 | self.depth = depth
14 | }
15 |
16 | public init?(languageLayer: LanguageLayer) {
17 | guard let tree = languageLayer.state.tree?.copy() else { return nil }
18 |
19 | self.init(tree: tree, data: languageLayer.languageConfig.data, depth: languageLayer.depth)
20 | }
21 |
22 | public mutating func applyEdit(_ edit: InputEdit) {
23 | self.tree = tree.edit(edit)!
24 | }
25 |
26 | var rangeSet: IndexSet {
27 | var set = IndexSet()
28 |
29 | for tsRange in tree.includedRanges {
30 | let range = tsRange.bytes.range
31 |
32 | set.insert(integersIn: Range(range) ?? 0..<0)
33 | }
34 |
35 | return set
36 | }
37 |
38 | func queryTarget(for queryDef: Query.Definition) throws -> LanguageLayerQueryCursor.Target {
39 | guard let query = data.queries[queryDef] else {
40 | throw LanguageLayerError.queryUnavailable(data.name, queryDef)
41 | }
42 |
43 | return .init(tree: tree, query: query, depth: depth, name: data.name)
44 | }
45 | }
46 |
47 | extension LanguageLayerSnapshot: Queryable {
48 | /// Run a query against the snapshot.
49 | public func executeQuery(_ queryDef: Query.Definition, in set: IndexSet) throws -> LanguageLayerQueryCursor {
50 | let target = try queryTarget(for: queryDef)
51 |
52 | return LanguageLayerQueryCursor(target: target, set: set)
53 | }
54 | }
55 |
56 | public struct LanguageLayerTreeSnapshot: Sendable {
57 | public private(set) var rootSnapshot: LanguageLayerSnapshot
58 | public private(set) var sublayerSnapshots: [LanguageLayerTreeSnapshot]
59 |
60 | public mutating func applyEdit(_ edit: InputEdit) {
61 | rootSnapshot.applyEdit(edit)
62 |
63 | for index in sublayerSnapshots.indices {
64 | sublayerSnapshots[index].applyEdit(edit)
65 | }
66 | }
67 |
68 | public func enumerateSnapshots(in set: IndexSet, block: (LanguageLayerSnapshot) throws -> Void) rethrows {
69 | // using set to filter out matches here doesn't actually work, because it is possible that a parent query match expansion will result in an intersection that otherwise would not happen
70 |
71 | try block(rootSnapshot)
72 |
73 | for sublayer in sublayerSnapshots {
74 | try sublayer.enumerateSnapshots(in: set, block: block)
75 | }
76 | }
77 |
78 | private func queryTargets(in set: IndexSet, for queryDef: Query.Definition) throws -> [LanguageLayerQueryCursor.Target] {
79 | var targets = [LanguageLayerQueryCursor.Target]()
80 |
81 | try enumerateSnapshots(in: set) { snapshot in
82 | let target = try snapshot.queryTarget(for: queryDef)
83 |
84 | targets.append(target)
85 | }
86 |
87 | return targets
88 | }
89 | }
90 |
91 | extension LanguageLayerTreeSnapshot: Queryable {
92 | public func executeQuery(_ queryDef: Query.Definition, in set: IndexSet) throws -> LanguageTreeQueryCursor {
93 | let targets = try queryTargets(in: set, for: queryDef)
94 |
95 | return LanguageTreeQueryCursor(set: set, targets: targets)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Projects/SwiftTreeSitterExample/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import SwiftTreeSitter
4 | import SwiftTreeSitterLayer
5 | import TreeSitterMarkdown
6 | import TreeSitterMarkdownInline
7 | import TreeSitterSwift
8 |
9 | struct ContentView: View {
10 | var body: some View {
11 | VStack {
12 | Image(systemName: "globe")
13 | .imageScale(.large)
14 | .foregroundColor(.accentColor)
15 | Text("Hello, world!")
16 | }
17 | .padding()
18 | .onAppear {
19 | do {
20 | // try runTreeSitterTest()
21 | try runTreeSitterDocumentTest()
22 | } catch {
23 | print("error: ", error)
24 | }
25 | }
26 | }
27 |
28 | func runTreeSitterTest() throws {
29 | let swiftConfig = try LanguageConfiguration(tree_sitter_swift(), name: "Swift")
30 |
31 | let parser = Parser()
32 | try parser.setLanguage(swiftConfig.language)
33 |
34 | let input = """
35 | func main() {}
36 | """
37 | let tree = parser.parse(input)!
38 |
39 | let query = swiftConfig.queries[.highlights]!
40 |
41 | let cursor = query.execute(in: tree)
42 | let highlights = cursor
43 | .resolve(with: .init(string: input))
44 | .highlights()
45 |
46 | for namedRange in highlights {
47 | print("range: ", namedRange)
48 | }
49 | }
50 |
51 | func runTreeSitterDocumentTest() throws {
52 | let markdownConfig = try LanguageConfiguration(tree_sitter_markdown(), name: "Markdown")
53 | let markdownInlineConfig = try LanguageConfiguration(
54 | tree_sitter_markdown_inline(),
55 | name: "MarkdownInline",
56 | bundleName: "TreeSitterMarkdown_TreeSitterMarkdownInline"
57 | )
58 | let swiftConfig = try LanguageConfiguration(tree_sitter_swift(), name: "Swift")
59 |
60 | let config = LanguageLayer.Configuration(
61 | languageProvider: {
62 | name in
63 | switch name {
64 | case "markdown":
65 | return markdownConfig
66 | case "markdown_inline":
67 | return markdownInlineConfig
68 | case "swift":
69 | return swiftConfig
70 | default:
71 | return nil
72 | }
73 | }
74 | )
75 |
76 | let rootLayer = try! LanguageLayer(languageConfig: markdownConfig, configuration: config)
77 |
78 | let source = """
79 | # this is markdown
80 |
81 | ```swift
82 | func main(a: Int) {
83 | }
84 | ```
85 |
86 | ## also markdown
87 |
88 | ```swift
89 | let value = "abc"
90 | ```
91 | """
92 |
93 | rootLayer.replaceContent(with: source)
94 |
95 | let fullRange = NSRange(source.startIndex.. QueryMatch? {
103 | while let match = nextMatch() {
104 | if match.allowed(in: context) == false {
105 | continue
106 | }
107 |
108 | return match
109 | }
110 |
111 | return nil
112 | }
113 |
114 | private func nextMatch() -> QueryMatch? {
115 | // use the cursor directly if we haven't prefetched
116 | if matches.isEmpty {
117 | return cursor.next()
118 | }
119 |
120 | if index >= matches.endIndex {
121 | return nil
122 | }
123 |
124 | let queryMatch = matches[index]
125 |
126 | index += 1
127 |
128 | return queryMatch
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Tests/SwiftTreeSitterTests/ParserTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import SwiftTreeSitter
4 | import TestTreeSitterSwift
5 |
6 | final class ParserTests: XCTestCase {
7 | func testSetIncludedRanges() {
8 | let parser = Parser()
9 |
10 | let range = TSRange(points: Point(row: 1, column: 0).. Tree? {
23 | guard let copiedTree = ts_tree_copy(self.internalTree) else {
24 | return nil
25 | }
26 |
27 | return Tree(internalTree: copiedTree)
28 | }
29 |
30 | public func mutableCopy() -> MutableTree? {
31 | guard let tree = copy() else { return nil }
32 |
33 | return MutableTree(tree: tree)
34 | }
35 |
36 | // Create a new Tree with the edit applied.
37 | public func edit(_ inputEdit: InputEdit) -> Tree? {
38 | guard let copiedTree = ts_tree_copy(self.internalTree) else {
39 | return nil
40 | }
41 |
42 | withUnsafePointer(to: inputEdit.internalInputEdit) { (ptr) -> Void in
43 | ts_tree_edit(copiedTree, ptr)
44 | }
45 |
46 | return Tree(internalTree: copiedTree)
47 | }
48 | }
49 |
50 | extension Tree {
51 | public var rootNode: Node? {
52 | let node = ts_tree_root_node(internalTree)
53 |
54 | return Node(internalNode: node, internalTree: self)
55 | }
56 |
57 | public var includedRanges: [TSRange] {
58 | var count: UInt32 = 0
59 |
60 | guard let tsRanges = ts_tree_included_ranges(internalTree, &count) else {
61 | return []
62 | }
63 |
64 | let bufferPointer = UnsafeBufferPointer(start: tsRanges, count: Int(count))
65 |
66 | // there is a bug in the current tree sitter version
67 | // that can produce ranges with invalid points (but seemingly correct) byte
68 | // offsets. We have to be more careful with those.
69 | let ranges = bufferPointer.map({ TSRange(potentiallyInvalidRange: $0) })
70 |
71 | free(tsRanges)
72 |
73 | return ranges
74 | }
75 | }
76 |
77 | extension Tree {
78 | public func changedRanges(from other: Tree) -> [TSRange] {
79 | var count: UInt32 = 0
80 |
81 | guard let tsRanges = ts_tree_get_changed_ranges(internalTree, other.internalTree, &count) else {
82 | return []
83 | }
84 |
85 | let bufferPointer = UnsafeBufferPointer(start: tsRanges, count: Int(count))
86 |
87 | // there is a bug in the current tree sitter version
88 | // that can produce ranges with invalid points (but seemingly correct) byte
89 | // offsets. We have to be more careful with those.
90 | let ranges = bufferPointer.map({ TSRange(potentiallyInvalidRange: $0) })
91 |
92 | free(tsRanges)
93 |
94 | return ranges
95 | }
96 |
97 | public func changedRanges(from other: MutableTree) -> [TSRange] {
98 | changedRanges(from: other.tree)
99 | }
100 | }
101 |
102 | extension Tree {
103 | public func enumerateNodes(in byteRange: Range, block: (Node) throws -> Void) rethrows {
104 | guard let root = rootNode else { return }
105 |
106 | guard let node = root.descendant(in: byteRange) else { return }
107 |
108 | try block(node)
109 |
110 | let cursor = node.treeCursor
111 |
112 | if cursor.goToFirstChild(for: byteRange.lowerBound) == false {
113 | return
114 | }
115 |
116 | try cursor.enumerateCurrentAndDescendents(block: block)
117 |
118 | while cursor.gotoNextSibling() {
119 | guard let node = cursor.currentNode else {
120 | assertionFailure("no current node when gotoNextSibling succeeded")
121 | break
122 | }
123 |
124 | // once we are past the interesting range, stop
125 | if node.byteRange.lowerBound > byteRange.upperBound {
126 | break
127 | }
128 |
129 | try cursor.enumerateCurrentAndDescendents(block: block)
130 | }
131 | }
132 | }
133 |
134 | public final class MutableTree {
135 | let tree: Tree
136 |
137 | init(internalTree: OpaquePointer,source: String?) {
138 | self.tree = Tree(internalTree: internalTree,source: source)
139 | }
140 |
141 | init(tree: Tree) {
142 | self.tree = tree
143 | }
144 |
145 | public func copy() -> Tree? {
146 | tree.copy()
147 | }
148 |
149 | public func mutableCopy() -> MutableTree? {
150 | guard let tree = copy() else { return nil }
151 |
152 | return MutableTree(tree: tree)
153 | }
154 | }
155 |
156 | extension MutableTree {
157 | public var rootNode: Node? {
158 | tree.rootNode
159 | }
160 |
161 | public var includedRanges: [TSRange] {
162 | tree.includedRanges
163 | }
164 | }
165 |
166 | extension MutableTree {
167 | public func edit(_ inputEdit: InputEdit) {
168 | withUnsafePointer(to: inputEdit.internalInputEdit) { (ptr) -> Void in
169 | ts_tree_edit(tree.internalTree, ptr)
170 | }
171 | }
172 |
173 | public func changedRanges(from other: Tree) -> [TSRange] {
174 | tree.changedRanges(from: other)
175 | }
176 |
177 | public func changedRanges(from other: MutableTree) -> [TSRange] {
178 | tree.changedRanges(from: other.tree)
179 | }
180 | }
181 |
182 | extension MutableTree {
183 | public func enumerateNodes(in byteRange: Range, block: (Node) throws -> Void) rethrows {
184 | try tree.enumerateNodes(in: byteRange, block: block)
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Documentation.docc/Tutorials/Resolving-Queries.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 10) {
2 | @Intro(title: "Fully Resolving Queries") {
3 | While powerful, tree-sitter's query language cannot describe all syntax tree states. To help expand its capabilities, it allows for embedding arbitrary statements. These provide an additional way to filter matches. However, tree-sitter's built-in query system does not actually evaluate these statements. Not all queries use them, but if predicates are present, the built-in types ignore them.
4 |
5 | `ResolvingQueryMatchSequence` is a `Sequence` that wraps the `QueryCursor` type and can transparently evaluate and filter results using query predicates. You can read more about predicates in tree-sitter's [query documentation](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries).
6 | }
7 |
8 | @Section(title: "Understanding Predicates") {
9 | @ContentAndMedia {
10 | It isn't necessary to fully understand the tree-sitter query syntax, but it can be helpful to see a little to understand what queries and predicates do. Let's take a look at a small section of Swift's highlight queries.
11 | }
12 |
13 | @Steps {
14 | @Step {
15 | Swift's highlights.scm contains a statement matching `navigation_expression`.
16 |
17 | This is matching any child `simple_identifier` node and giving it the label `@type`. This is explained in the comment at the top. However, this query on its own will match too many syntax constructs.
18 |
19 | @Code(name: "highlights.scm", file: "using-queries-1-1-highlights.scm")
20 | }
21 |
22 | @Step {
23 | Expand the query's S-expression-style parentheses.
24 |
25 | @Code(name: "highlights.scm", file: "using-queries-1-2-highlights.scm")
26 | }
27 |
28 | @Step {
29 | Add a `#match` predicate.
30 |
31 | This restricts `@label` to only capture values that match a regular expression.
32 |
33 | @Code(name: "highlights.scm", file: "using-queries-1-3-highlights.scm")
34 | }
35 | }
36 | }
37 |
38 | @Section(title: "Enumerating Matches with Cursor") {
39 | @ContentAndMedia {
40 | Let's see how this pattern matches Swift code using a standard `Cursor`.
41 | }
42 |
43 | @Steps {
44 | @Step {
45 | Set up our language and parser as before.
46 |
47 | We're just using different source text.
48 |
49 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-1-example.swift")
50 | }
51 |
52 | @Step {
53 | Build our query object.
54 |
55 | Remember, this can be expensive.
56 |
57 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-2-example.swift")
58 | }
59 |
60 | @Step {
61 | Execute the query on the tree, returning a cursor.
62 |
63 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-3-example.swift")
64 | }
65 |
66 | @Step {
67 | Create a new variable to hold the results of the cursor.
68 |
69 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-4-example.swift")
70 | }
71 |
72 | @Step {
73 | Extract just the captures.
74 |
75 | Captures are nodes with labels, starting with the `@` symbol in the query pattern.
76 |
77 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-5-example.swift")
78 | }
79 |
80 | @Step {
81 | Convert it into a flat array.
82 |
83 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-6-example.swift")
84 | }
85 |
86 | @Step {
87 | Print out all of the matching ranges.
88 |
89 | Because `Cursor` does not evaluate predicates, we've matched too many nodes.
90 |
91 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-7-example.swift") {
92 | @Image(source: "cursor-output.png", alt: "Two ranges {21, 8} and {40, 8}")
93 | }
94 | }
95 | }
96 | }
97 |
98 | @Section(title: "Enumerating Matches with a Resolved Sequence") {
99 | @ContentAndMedia {
100 | A `Cursor` must be transformed into a `ResolvingQueryMatchSequence` to evaluate predicates and further filter matches. This evaluation requires access to the text content.
101 | }
102 |
103 | @Steps {
104 | @Step {
105 | Start with our previous implementation.
106 |
107 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-7-example.swift")
108 | }
109 | @Step {
110 | Create a `ResolvingQueryMatchSequence` from the cursor.
111 |
112 | To actually resolve the results, the sequence requires some context. We're building one from the source string data. But, there are other ways to do, and those are required if you need to evaluate subqueries.
113 |
114 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-8-example.swift")
115 | }
116 |
117 | @Step {
118 | Swap the `cursor` for our new `resolvingSequence`.
119 |
120 | We now get one match, just like we'd expect.
121 |
122 | @Code(name: "TreeSitterExample.swift", file: "using-queries-2-9-example.swift") {
123 | @Image(source: "resolved-output.png", alt: "One range {21, 8}")
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/QueryDefinitions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Query {
4 | public enum Definition: Hashable, Sendable {
5 | case injections
6 | case highlights
7 | case locals
8 | case custom(String)
9 |
10 | public var name: String {
11 | switch self {
12 | case .injections:
13 | return "injections"
14 | case .highlights:
15 | return "highlights"
16 | case .locals:
17 | return "locals"
18 | case .custom(let value):
19 | return value
20 | }
21 | }
22 |
23 | public var filename: String {
24 | "\(name).scm"
25 | }
26 | }
27 | }
28 |
29 | /// A combined name and range
30 | ///
31 | /// Useful for generalizing data from query matches.
32 | public struct NamedRange: Codable, Equatable, Sendable, Hashable {
33 | public let nameComponents: [String]
34 | public let tsRange: TSRange
35 |
36 | public init(nameComponents: [String], tsRange: TSRange) {
37 | self.nameComponents = nameComponents
38 | self.tsRange = tsRange
39 | }
40 |
41 | public init(name: String, tsRange: TSRange) {
42 | let components = name.split(separator: ".").map(String.init)
43 |
44 | self.init(nameComponents: components, tsRange: tsRange)
45 | }
46 |
47 | public init (name: String, range: NSRange, pointRange: Range = Point.zero.. Bool {
65 | if lhs.tsRange != rhs.tsRange {
66 | return lhs.tsRange < rhs.tsRange
67 | }
68 |
69 | return lhs.nameComponents.count < rhs.nameComponents.count
70 | }
71 | }
72 |
73 | extension NamedRange: CustomDebugStringConvertible {
74 | public var debugDescription: String {
75 | "<\"\(name)\": \(tsRange)>"
76 | }
77 | }
78 |
79 | extension QueryMatch {
80 | /// Interpret the match using the "injections.scm" definition
81 | ///
82 | /// - `injection.content` defines the range of the injection
83 | /// - a node with `injection.language` specifies the value of the language in the text
84 | /// - if that is not present, uses `injection.language` metadata
85 | ///
86 | /// If `textProvider` is nil and a node contents is needed, the injection is dropped.
87 | public func injection(with textProvider: Predicate.TextProvider) -> NamedRange? {
88 | guard let contentCapture = captures(named: "injection.content").first else {
89 | return nil
90 | }
91 |
92 | let languageCapture = captures(named: "injection.language").first
93 |
94 | let nodeLanguage: String?
95 |
96 | if let node = languageCapture?.node {
97 | nodeLanguage = textProvider(node.range, node.pointRange)
98 | } else {
99 | nodeLanguage = nil
100 | }
101 |
102 | let setLanguage = metadata["injection.language"]
103 |
104 | guard let language = nodeLanguage ?? setLanguage else {
105 | return nil
106 | }
107 |
108 | return NamedRange(nameComponents: [language], tsRange: contentCapture.node.tsRange)
109 | }
110 | }
111 |
112 | extension QueryCapture {
113 | /// Interpret the capture using the "highlights.scm" definition
114 | ///
115 | /// Capture names are used without modification.
116 | public var highlight: NamedRange? {
117 | let components = nameComponents
118 | guard components.isEmpty == false else { return nil }
119 |
120 | return NamedRange(nameComponents: components, tsRange: node.tsRange)
121 | }
122 | }
123 |
124 | extension QueryCapture {
125 | public var locals: NamedRange? {
126 | return highlight
127 | }
128 | }
129 |
130 | extension Sequence where Element == QueryMatch {
131 | private var captures: [QueryCapture] {
132 | map({ $0.captures })
133 | .flatMap({ $0 })
134 | }
135 |
136 | /// Interpret matches using the "highlights.scm" definition
137 | ///
138 | /// Results are sorted such that less-specific matches come before more-specific. This helps to resolve ambiguous patterns.
139 | public func highlights() -> [NamedRange] {
140 | captures
141 | .sorted()
142 | .compactMap { $0.highlight }
143 | }
144 |
145 | /// Interpret the match using the "injections.scm" definition.
146 | ///
147 | /// - `injection.content` defines the range of the injection
148 | /// - a node with `injection.language` specifies the value of the language in the text
149 | /// - if that is not present, uses `injection.language` metadata
150 | ///
151 | /// If `textProvider` returns nil and node contents is needed, the injection is dropped.
152 | public func injections(with textProvider: Predicate.TextProvider) -> [NamedRange] {
153 | return compactMap({ $0.injection(with: textProvider) })
154 | }
155 |
156 | /// Interpret the cursor using the "locals.scm" definition
157 | public func locals() -> [NamedRange] {
158 | captures
159 | .compactMap({ $0.locals })
160 | }
161 | }
162 |
163 | @available(*, deprecated, message: "Please use ResolvingQueryMatchSequence")
164 | extension ResolvingQueryCursor {
165 | public func injections() -> [NamedRange] {
166 | return compactMap({ $0.injection(with: context.textProvider) })
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Parser.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | enum ParserError: Error {
5 | case languageIncompatible
6 | case languageFailure
7 | case languageInvalid
8 | case unsupportedEncoding(String.Encoding)
9 | }
10 |
11 | public class Parser {
12 | private let internalParser: OpaquePointer
13 | private let encoding: String.Encoding
14 |
15 | public init() {
16 | self.internalParser = ts_parser_new()
17 | self.encoding = String.nativeUTF16Encoding
18 | }
19 |
20 | deinit {
21 | ts_parser_delete(internalParser)
22 | }
23 | }
24 |
25 | extension Parser {
26 | /// Access the parser's language
27 | ///
28 | /// Setting a language via this property isn't possible because that operation is failable. Please use `setLanguage`.
29 | public var language: Language? {
30 | get {
31 | return ts_parser_language(internalParser).map { Language(language: $0) }
32 | }
33 | }
34 |
35 | public func setLanguage(_ language: Language) throws {
36 | try setLanguage(language.tsLanguage)
37 | }
38 |
39 | public func setLanguage(_ language: OpaquePointer) throws {
40 | let success = ts_parser_set_language(internalParser, language)
41 |
42 | if success == false {
43 | throw ParserError.languageFailure
44 | }
45 | }
46 |
47 | /// Resets the parser to begin at the beginning of the document.
48 | ///
49 | /// If the parser was cancelled or timed out, use this to reset it.
50 | public func reset() {
51 | ts_parser_reset(internalParser)
52 | }
53 |
54 | /// The ranges this parser will operate on.
55 | ///
56 | /// This defaults to the entire document. This is useful for working with embedded languages.
57 | ///
58 | /// > Warning: These values must be manually updated, and must be in ascending order. The `includedRanges` property of `Tree` can be useful for this, as it is updtaed when edits are applied.
59 | public var includedRanges: [TSRange] {
60 | get {
61 | var count: UInt32 = 0
62 | let tsRangePointer = ts_parser_included_ranges(internalParser, &count)
63 |
64 | let tsRangeBuffer = UnsafeBufferPointer(start: tsRangePointer, count: Int(count))
65 |
66 | return tsRangeBuffer.map({ TSRange(internalRange: $0) })
67 | }
68 | set {
69 | let ranges = newValue.map({ $0.internalRange })
70 |
71 | ranges.withUnsafeBytes { bufferPtr in
72 | let count = newValue.count
73 |
74 | guard let ptr = bufferPtr.baseAddress?.bindMemory(to: TreeSitter.TSRange.self, capacity: count) else {
75 | preconditionFailure("unable to convert pointer")
76 | }
77 |
78 | ts_parser_set_included_ranges(internalParser, ptr, UInt32(count))
79 | }
80 | }
81 | }
82 |
83 | /// The maximum time interval the parser can run before halting.
84 | public var timeout: TimeInterval {
85 | get {
86 | let us = ts_parser_timeout_micros(internalParser)
87 |
88 | return TimeInterval(us) / 1000.0 / 1000.0
89 | }
90 | set {
91 | let us = UInt64(newValue * 1000.0 * 1000.0)
92 |
93 | ts_parser_set_timeout_micros(internalParser, us)
94 | }
95 | }
96 | }
97 |
98 | extension Parser {
99 | public typealias ReadBlock = (Int, Point) -> Data?
100 | public typealias DataSnapshotProvider = @Sendable (Int, Point) -> Data?
101 |
102 | public func parse(_ source: String) -> MutableTree? {
103 | guard let data = source.data(using: encoding) else { return nil }
104 |
105 | let dataLength = data.count
106 |
107 | let optionalTreePtr = data.withUnsafeBytes({ (byteBuffer) -> OpaquePointer? in
108 | guard let ptr = byteBuffer.baseAddress?.bindMemory(to: Int8.self, capacity: dataLength) else {
109 | return nil
110 | }
111 |
112 | return ts_parser_parse_string_encoding(internalParser, nil, ptr, UInt32(dataLength), TSInputEncodingUTF16LE)
113 | })
114 |
115 | return optionalTreePtr.flatMap({ MutableTree(internalTree: $0,source: source) })
116 | }
117 |
118 | public func parse(tree: Tree?, encoding: TSInputEncoding = TSInputEncodingUTF16LE, readBlock: ReadBlock) -> MutableTree? {
119 | return withoutActuallyEscaping(readBlock) { escapingClosure in
120 | let input = Input(encoding: encoding, readBlock: escapingClosure)
121 |
122 | guard let internalInput = input.internalInput else {
123 | return nil
124 | }
125 |
126 | guard let newTree = ts_parser_parse(internalParser, tree?.internalTree, internalInput) else {
127 | return nil
128 | }
129 |
130 | return MutableTree(internalTree: newTree,source: tree?.source)
131 | }
132 | }
133 |
134 | public func parse(tree: MutableTree?, encoding: TSInputEncoding = TSInputEncodingUTF16LE, readBlock: ReadBlock) -> MutableTree? {
135 | parse(tree: tree?.tree, encoding: encoding, readBlock: readBlock)
136 | }
137 |
138 | public func parse(tree: Tree?, string: String, limit: Int? = nil, chunkSize: Int = 2048) -> MutableTree? {
139 | let readFunction = Parser.readFunction(for: string, limit: limit, chunkSize: chunkSize)
140 |
141 | return parse(tree: tree, readBlock: readFunction)
142 | }
143 |
144 | public func parse(tree: MutableTree?, string: String, limit: Int? = nil, chunkSize: Int = 2048) -> MutableTree? {
145 | parse(tree: tree?.tree, string: string, limit: limit, chunkSize: chunkSize)
146 | }
147 |
148 | /// Form a function that captures an immutable view into the data of a `String`.
149 | public static func readFunction(for string: String, limit: Int? = nil, chunkSize: Int = 2048) -> Parser.DataSnapshotProvider {
150 | let usableLimit = limit ?? string.utf16.count
151 | let encoding = String.nativeUTF16Encoding
152 |
153 | return { (start, _) -> Data? in
154 | return string.data(
155 | at: start,
156 | limit: usableLimit,
157 | using: encoding,
158 | chunkSize: chunkSize
159 | )
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | support@chimehq.com.
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/SwiftTreeSitter.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
71 |
77 |
78 |
79 |
85 |
91 |
92 |
93 |
94 |
95 |
102 |
103 |
109 |
110 |
111 |
112 |
114 |
120 |
121 |
122 |
124 |
130 |
131 |
132 |
133 |
134 |
144 |
145 |
151 |
152 |
158 |
159 |
160 |
161 |
163 |
164 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Node.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Node.swift
3 | // SwiftTreeSitter
4 | //
5 | // Created by Matt Massicotte on 2018-12-17.
6 | // Copyright © 2018 Chime Systems. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import TreeSitter
11 |
12 | public struct Node {
13 | let internalNode: TSNode
14 | let internalTree: Tree
15 |
16 | init?(internalNode: TSNode, internalTree: Tree) {
17 | if ts_node_is_null(internalNode) {
18 | return nil
19 | }
20 |
21 | self.internalNode = internalNode
22 | self.internalTree = internalTree
23 | }
24 | }
25 |
26 | extension Node: CustomDebugStringConvertible {
27 | public var debugDescription: String {
28 | let typeName = nodeType ?? "unnamed"
29 |
30 | return "<\(typeName) range: \(range) childCount: \(childCount)>"
31 | }
32 | }
33 |
34 | extension Node: Equatable {
35 | public static func == (lhs: Node, rhs: Node) -> Bool {
36 | return ts_node_eq(lhs.internalNode, rhs.internalNode)
37 | }
38 | }
39 |
40 | extension Node {
41 | public var sExpressionString: String? {
42 | guard let str = ts_node_string(internalNode) else {
43 | return nil
44 | }
45 |
46 | let string = String(cString: str)
47 |
48 | free(str)
49 |
50 | return string
51 | }
52 |
53 | public var nodeType: String? {
54 | guard let str = ts_node_type(internalNode) else {
55 | return nil
56 | }
57 |
58 | return String(cString: str)
59 | }
60 |
61 | public var id: UInt {
62 | return UInt(bitPattern: internalNode.id)
63 | }
64 |
65 | public var symbol: Int {
66 | return Int(ts_node_symbol(internalNode))
67 | }
68 |
69 | public var range: NSRange {
70 | return byteRange.range
71 | }
72 |
73 | public var byteRange: Range {
74 | let start = ts_node_start_byte(internalNode)
75 | let end = ts_node_end_byte(internalNode)
76 |
77 | return start.. {
81 | let start = ts_node_start_point(internalNode)
82 | let end = ts_node_end_point(internalNode)
83 |
84 | return Point(internalPoint: start).. Node? {
116 | let count = UInt32(fieldName.utf8.count)
117 | let n = ts_node_child_by_field_name(internalNode, fieldName, count)
118 | return Node(internalNode: n, internalTree: internalTree)
119 | }
120 |
121 | public var childCount: Int {
122 | return Int(ts_node_child_count(internalNode))
123 | }
124 |
125 | public var namedChildCount: Int {
126 | return Int(ts_node_named_child_count(internalNode))
127 | }
128 |
129 | public func child(at index: Int) -> Node? {
130 | let n = ts_node_child(internalNode, UInt32(index))
131 |
132 | return Node(internalNode: n, internalTree: internalTree)
133 | }
134 |
135 | public func namedChild(at index: Int) -> Node? {
136 | let n = ts_node_named_child(internalNode, UInt32(index))
137 |
138 | return Node(internalNode: n, internalTree: internalTree)
139 | }
140 |
141 | public func fieldNameForChild(at index: Int) -> String? {
142 | let name = ts_node_field_name_for_child(internalNode, UInt32(index))
143 |
144 | guard let name else { return nil }
145 | return String(cString: name)
146 | }
147 |
148 | public var parent: Node? {
149 | let n = ts_node_parent(internalNode)
150 |
151 | return Node(internalNode: n, internalTree: internalTree)
152 | }
153 |
154 | public var nextSibling: Node? {
155 | let n = ts_node_next_sibling(internalNode)
156 |
157 | return Node(internalNode: n, internalTree: internalTree)
158 | }
159 |
160 | public var previousSibling: Node? {
161 | let n = ts_node_prev_sibling(internalNode)
162 |
163 | return Node(internalNode: n, internalTree: internalTree)
164 | }
165 |
166 | public var nextNamedSibling: Node? {
167 | let n = ts_node_next_named_sibling(internalNode)
168 |
169 | return Node(internalNode: n, internalTree: internalTree)
170 | }
171 |
172 | public var previousNamedSibling: Node? {
173 | let n = ts_node_prev_named_sibling(internalNode)
174 |
175 | return Node(internalNode: n, internalTree: internalTree)
176 | }
177 |
178 | public func descendant(in pointRange: Range) -> Node? {
179 | let lower = pointRange.lowerBound
180 | let upper = pointRange.upperBound
181 |
182 | let n = ts_node_descendant_for_point_range(internalNode, lower.internalPoint, upper.internalPoint)
183 |
184 | return Node(internalNode: n, internalTree: internalTree)
185 | }
186 |
187 | public func descendant(in byteRange: Range) -> Node? {
188 | let lower = byteRange.lowerBound
189 | let upper = byteRange.upperBound
190 |
191 | let n = ts_node_descendant_for_byte_range(internalNode, lower, upper)
192 |
193 | return Node(internalNode: n, internalTree: internalTree)
194 | }
195 |
196 | /// The text of the node
197 | public var text: String? {
198 | // Tree.source holds the original source text. It can be empty if the parse
199 | // was performed without a backing string; handle that gracefully.
200 | let source = self.internalTree.source
201 | guard source != nil else { return nil }
202 |
203 | // byteRange is in UTF-16LE bytes. Convert to UTF-16 code unit offsets.
204 | let lowerUnits = Int(byteRange.lowerBound / 2)
205 | let upperUnits = Int(byteRange.upperBound / 2)
206 |
207 | // Clamp to valid bounds of the source’s UTF-16 view.
208 | let utf16Count = source!.utf16.count
209 | let startUnits = max(0, min(lowerUnits, utf16Count))
210 | let endUnits = max(startUnits, min(upperUnits, utf16Count))
211 |
212 | // Convert UTF-16 code unit offsets to String.Index and slice.
213 | let startIndex = String.Index(utf16Offset: startUnits, in: source!)
214 | let endIndex = String.Index(utf16Offset: endUnits, in: source!)
215 |
216 | return String(source![startIndex.. Void) {
222 | for i in 0.. URL? {
124 | #if os(macOS) || targetEnvironment(macCatalyst)
125 | let bundlePath = bundleContainerURL?.appendingPathComponent("\(bundleName).bundle", isDirectory: true)
126 |
127 | guard let bundlePath else { return nil }
128 |
129 | // Depending on how you compile a macOS executable, this path varies.
130 | // Couldn't nail down when with compiler flags
131 | // (Xcode seems to create the longer paths, SwiftPM the shorter ones)
132 | let shortQueriesPath = bundlePath.appendingPathComponent("queries", isDirectory: true)
133 | if FileManager.default.fileExists(atPath: shortQueriesPath.path) {
134 | return shortQueriesPath
135 | }
136 | return bundlePath.appendingPathComponent("Contents/Resources/queries", isDirectory: true)
137 | #elseif os(iOS) || os(tvOS) || os(visionOS) || os(watchOS)
138 | let bundlePath = bundleContainerURL?.appendingPathComponent("\(bundleName).bundle", isDirectory: true)
139 |
140 | return bundlePath?.appendingPathComponent("queries", isDirectory: true)
141 | #else
142 | // Linux and Windows use .resources instead of .bundle
143 | let resourcePath = bundleContainerURL?.appendingPathComponent("\(bundleName).resources", isDirectory: true)
144 |
145 | return resourcePath?.appendingPathComponent("queries", isDirectory: true)
146 | #endif
147 | }
148 | }
149 |
150 | extension Query {
151 | static func query(definition: Query.Definition, for language: Language, in url: URL) throws -> Query? {
152 | let fullURL = url.appendingPathComponent(definition.filename).standardizedFileURL
153 |
154 | guard FileManager.default.isReadableFile(atPath: fullURL.path) else {
155 | return nil
156 | }
157 |
158 | return try Query(language: language, url: fullURL)
159 | }
160 |
161 | static func queries(for language: Language, in url: URL) throws -> [Query.Definition: Query] {
162 | var queries = [Query.Definition: Query]()
163 |
164 | if let query = try Self.query(definition: .injections, for: language, in: url) {
165 | queries[.injections] = query
166 | }
167 |
168 | if let query = try Self.query(definition: .highlights, for: language, in: url) {
169 | queries[.highlights] = query
170 | }
171 |
172 | if let query = try Self.query(definition: .locals, for: language, in: url) {
173 | queries[.locals] = query
174 | }
175 |
176 | return queries
177 | }
178 | }
179 | #endif
180 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Predicate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | public enum QueryPredicateStep: Hashable, Sendable {
5 | case done
6 | case capture(String)
7 | case string(String)
8 | }
9 |
10 | extension QueryPredicateStep: CustomStringConvertible {
11 | public var description: String {
12 | switch self {
13 | case .done:
14 | return ""
15 | case .capture(let v):
16 | return ""
17 | case .string(let v):
18 | return ""
19 | }
20 | }
21 | }
22 |
23 | public enum Predicate: Hashable, Sendable {
24 | case eq([String], captureNames: [String])
25 | case notEq([String], captureNames: [String])
26 | case match(NSRegularExpression, captureNames: [String])
27 | case notMatch(NSRegularExpression, captureNames: [String])
28 | case isNot(String)
29 | case anyOf(Set, captureName: String)
30 | case notAnyOf(Set, captureName: String)
31 | case set(captureName: String? = nil, key: String, value: String)
32 | case generic(String, strings: [String], captureNames: [String])
33 |
34 | /// Returns an array of capture names that the predicate references.
35 | ///
36 | /// This value can be empty.
37 | public var captureNames: [String] {
38 | switch self {
39 | case .eq(_, let names):
40 | return names
41 | case .notEq(_, let names):
42 | return names
43 | case .match(_, let names):
44 | return names
45 | case .notMatch(_, let names):
46 | return names
47 | case .isNot:
48 | return []
49 | case .anyOf(_, let names):
50 | return [names]
51 | case .notAnyOf(_, let names):
52 | return [names]
53 | case .set(let name, _, _):
54 | if let name = name {
55 | return [name]
56 | }
57 |
58 | return []
59 | case .generic(_, _, let names):
60 | return names
61 | }
62 | }
63 |
64 | /// Returns all of the `QueryCapture` instances that correspond to this predicate.
65 | public func captures(in match: QueryMatch) -> [QueryCapture] {
66 | let names = captureNames
67 |
68 | if names.isEmpty {
69 | return match.captures
70 | }
71 |
72 | return match.captures.filter({ capture in
73 | guard let name = capture.name else { return false }
74 |
75 | return names.contains(name)
76 | })
77 | }
78 | }
79 |
80 | extension Predicate {
81 | public typealias TextProvider = (NSRange, Range) -> String?
82 | public typealias TextSnapshotProvider = @Sendable (NSRange, Range) -> String?
83 | public typealias GroupMembershipProvider = (String, NSRange, Range) -> Bool
84 |
85 | public struct Context {
86 | public let textProvider: TextProvider
87 | public let groupMembershipProvider: GroupMembershipProvider
88 |
89 | @MainActor
90 | public static let none = Context(textProvider: { _, _ in return nil })
91 |
92 | public init(
93 | textProvider: @escaping TextProvider,
94 | groupMembershipProvider: @escaping GroupMembershipProvider = { _, _, _ in return false }
95 | ) {
96 | self.textProvider = textProvider
97 | self.groupMembershipProvider = groupMembershipProvider
98 | }
99 |
100 | /// Initialize with a constant string.
101 | public init(string: String) {
102 | self.init(
103 | textProvider: string.predicateTextProvider
104 | )
105 | }
106 |
107 | var cachingContext: Context {
108 | var cachedText = [NSRange : String]()
109 |
110 | // create a caching provider
111 | let cachingTextProvider: TextProvider = { (range, pointRange) in
112 | if let value = cachedText[range] {
113 | return value
114 | }
115 |
116 | let value = textProvider(range, pointRange)
117 |
118 | cachedText[range] = value
119 |
120 | return value
121 | }
122 |
123 | return Context(textProvider: cachingTextProvider, groupMembershipProvider: groupMembershipProvider)
124 | }
125 | }
126 |
127 | func allowsMatch(_ match: QueryMatch, context: Context) -> Bool {
128 | switch self {
129 | case .set, .generic:
130 | return true
131 | case .eq, .notEq, .anyOf, .notAnyOf, .match, .notMatch, .isNot:
132 | return captures(in: match).allSatisfy { capture in
133 | allowsMatch(for: capture, context: context)
134 | }
135 | }
136 | }
137 |
138 | func allowsMatch(for capture: QueryCapture, context: Context) -> Bool {
139 | allowsMatch(range: capture.node.range,
140 | pointRange: capture.node.pointRange,
141 | context: context)
142 | }
143 |
144 | func allowsMatch(range: NSRange, pointRange: Range, context: Context) -> Bool {
145 | switch self {
146 | case .set, .generic:
147 | return true
148 | case .isNot(let groupName):
149 | return context.groupMembershipProvider(groupName, range, pointRange) == false
150 | case .eq, .notEq, .anyOf, .notAnyOf, .match, .notMatch:
151 | guard let text = context.textProvider(range, pointRange) else {
152 | return false
153 | }
154 |
155 | return evalulate(with: text)
156 | }
157 | }
158 |
159 | public func evalulate(with text: String) -> Bool {
160 | switch self {
161 | case .eq(let strings, _):
162 | return strings.allSatisfy({ $0 == text })
163 | case .notEq(let strings, _):
164 | return strings.allSatisfy({ $0 != text })
165 | case .match(let exp, _):
166 | let range = NSRange(0.. [Predicate] {
196 | var predicates = [Predicate]()
197 |
198 | var stepList = steps
199 | while stepList.isEmpty == false {
200 | let (predicate, count) = try parseNextPredicate(stepList)
201 |
202 | predicates.append(predicate)
203 |
204 | stepList.removeFirst(count)
205 | }
206 |
207 | return predicates
208 | }
209 |
210 | func parseNextPredicate(_ steps: [QueryPredicateStep]) throws -> (Predicate, Int) {
211 | guard case .string(let name)? = steps.first else {
212 | throw PredicateParserError.stepNameExpected
213 | }
214 |
215 | guard let doneIndex = steps.firstIndex(of: .done) else {
216 | throw PredicateParserError.doneExpected
217 | }
218 |
219 | let args = Array(steps[1.. Predicate {
228 | var strings = [String]()
229 | var captures = [String]()
230 |
231 | for arg in argSteps {
232 | switch arg {
233 | case .capture(let value):
234 | captures.append(value)
235 | case .string(let value):
236 | strings.append(value)
237 | case .done:
238 | throw PredicateParserError.argumentsContainDone
239 | }
240 | }
241 |
242 | switch name {
243 | case "eq?":
244 | return .eq(strings, captureNames: captures)
245 | case "not-eq?":
246 | return .notEq(strings, captureNames: captures)
247 | case "match?":
248 | guard let pattern = strings.first else {
249 | return .generic(name, strings: strings, captureNames: captures)
250 | }
251 |
252 | let expression = try NSRegularExpression(pattern: pattern, options: [])
253 |
254 | return .match(expression, captureNames: captures)
255 | case "not-match?":
256 | guard let pattern = strings.first else {
257 | return .generic(name, strings: strings, captureNames: captures)
258 | }
259 |
260 | let expression = try NSRegularExpression(pattern: pattern, options: [])
261 |
262 | return .notMatch(expression, captureNames: captures)
263 | case "any-of?":
264 | guard let capture = captures.first else {
265 | return .generic(name, strings: strings, captureNames: captures)
266 | }
267 |
268 | return .anyOf(Set(strings), captureName: capture)
269 | case "not-any-of?":
270 | guard let capture = captures.first else {
271 | return .generic(name, strings: strings, captureNames: captures)
272 | }
273 |
274 | return .notAnyOf(Set(strings), captureName: capture)
275 | case "is-not?":
276 | guard let name = strings.first, strings.count == 1 else {
277 | return .generic(name, strings: strings, captureNames: captures)
278 | }
279 |
280 | return .isNot(name)
281 | case "set!":
282 | if strings.count != 2 || captures.count > 1 {
283 | return .generic(name, strings: strings, captureNames: captures)
284 | }
285 |
286 | return .set(captureName: captures.first, key: strings[0], value: strings[1])
287 | default:
288 | return .generic(name, strings: strings, captureNames: captures)
289 | }
290 | }
291 | }
292 |
293 | extension PredicateParser {
294 | func predicates(in query: OpaquePointer) throws -> [[Predicate]] {
295 | let patternCount = Int(ts_query_pattern_count(query))
296 |
297 | var predicates = [[Predicate]](repeating: [], count: patternCount)
298 |
299 | for i in 0.. [QueryPredicateStep] {
312 | var length: UInt32 = 0
313 |
314 | let steps = ts_query_predicates_for_pattern(query, UInt32(index), &length)
315 |
316 | let buffer = UnsafeBufferPointer(start: steps,
317 | count: Int(length))
318 |
319 | return try buffer.map { step -> QueryPredicateStep in
320 | let valueId = step.value_id
321 | var length: UInt32 = 0
322 |
323 | switch step.type {
324 | case TSQueryPredicateStepTypeCapture:
325 | guard let cStr = ts_query_capture_name_for_id(query, valueId, &length) else {
326 | throw QueryPredicateError.valueNotFound
327 | }
328 |
329 | return .capture(String(cString: cStr))
330 | case TSQueryPredicateStepTypeString:
331 | guard let cStr = ts_query_string_value_for_id(query, valueId, &length) else {
332 | throw QueryPredicateError.valueNotFound
333 | }
334 |
335 | return .string(String(cString: cStr))
336 | case TSQueryPredicateStepTypeDone:
337 | return .done
338 | default:
339 | throw QueryPredicateError.unrecognizedStepType
340 | }
341 | }
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/Tests/SwiftTreeSitterLayerTests/LanguageLayerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftTreeSitter
4 | import SwiftTreeSitterLayer
5 | import TestTreeSitterSwift
6 |
7 |
8 | extension Point {
9 | init(_ row: Int, _ column: Int) {
10 | self.init(row: row, column: column)
11 | }
12 | }
13 |
14 | #if !os(WASI)
15 | final class LanguageLayerTests: XCTestCase {
16 | private static let swiftConfig: LanguageConfiguration = {
17 | let language = Language(language: tree_sitter_swift())
18 |
19 | let queryText = """
20 | ["func"] @keyword.function
21 | ["var" "let"] @keyword
22 | """
23 |
24 | let highlightQuery = try! Query(language: language, data: queryText.data(using: .utf8)!)
25 |
26 | return LanguageConfiguration(language,
27 | name: "Swift",
28 | queries: [.highlights: highlightQuery])
29 | }()
30 |
31 | private static let selfInjectingSwiftConfig: LanguageConfiguration = {
32 | let queryText = """
33 | ((line_str_text) @injection.content (#set! injection.language "swift"))
34 | """
35 | let injectionQuery = try! Query(language: swiftConfig.language, data: queryText.data(using: .utf8)!)
36 |
37 | var queries = swiftConfig.queries
38 |
39 | queries[.injections] = injectionQuery
40 |
41 | return LanguageConfiguration(swiftConfig.language,
42 | name: swiftConfig.name,
43 | queries: queries)
44 | }()
45 | }
46 |
47 | extension LanguageLayerTests {
48 | func testExecuteQuery() throws {
49 | let config = LanguageLayer.Configuration()
50 | let tree = try LanguageLayer(languageConfig: Self.swiftConfig, configuration: config)
51 |
52 | let text = """
53 | func main() {
54 | }
55 | """
56 | tree.replaceContent(with: text)
57 |
58 | let cursor = try tree.executeQuery(.highlights, in: NSRange(0..
2 |
3 | [![Build Status][build status badge]][build status]
4 | [![Platforms][platforms badge]][platforms]
5 | [![Documentation][documentation badge]][documentation]
6 | [![Discord][discord badge]][discord]
7 |
8 |
9 |
10 | # SwiftTreeSitter
11 |
12 | Swift API for the [tree-sitter](https://tree-sitter.github.io/) incremental parsing system.
13 |
14 | - Close to full coverage of the C API
15 | - Swift/Foundation types where possible
16 | - Standard query result mapping for highlights and injections
17 | - Query predicate/directive support via `ResolvingQueryMatchSequence`
18 | - Nested language support
19 | - Swift concurrency support where possible
20 |
21 | # Structure
22 |
23 | This project is actually split into two parts: `SwiftTreeSitter` and `SwiftTreeSitterLayer`.
24 |
25 | The SwiftTreeSitter target is a close match to the C runtime API. It adds only a few additional types to help support querying. It is fairly low-level, and there will be significant work to use it in a real project.
26 |
27 | SwiftTreeSitterLayer is an abstraction built on top of SwiftTreeSitter. It supports documents with nested languages and transparent querying across those nestings. It also supports asynchronous language resolution. While still low-level, SwiftTreeSitterLayer is easier to work with while also supporting more features.
28 |
29 | And yet there's more! If you are looking a higher-level system for syntax highlighting and other syntactic operations, you might want to have a look at [Neon](https://github.com/ChimeHQ/Neon). It is much easier to integrate with a text system, and has lots of additional performance-related features.
30 |
31 |
32 | ## Integration
33 |
34 | ```swift
35 | dependencies: [
36 | .package(url: "https://github.com/ChimeHQ/SwiftTreeSitter")
37 | ],
38 | targets: [
39 | .target(
40 | name: "MySwiftTreeSitterTarget",
41 | dependencies: ["SwiftTreeSitter"]
42 | ),
43 | .target(
44 | name: "MySwiftTreeSitterLayerTarget",
45 | dependencies: [
46 | .product(name: "SwiftTreeSitterLayer", package: "SwiftTreeSitter"),
47 | ]
48 | ),
49 | ]
50 | ```
51 |
52 | ## Range Translation
53 |
54 | The tree-sitter runtime operates on raw string data. This means it works with bytes, and is string-encoding-sensitive. Swift's `String` type is an abstraction on top of raw data and cannot be used directly. To overcome this, you also have to be aware of the types of indexes you are using and how string data is translated back and forth.
55 |
56 | To help, SwiftTreeSitter supports the base tree-sitter encoding facilities. You can control this via `Parser.parse(tree:encoding:readBlock:)`. But, by default this will assume UTF-16-encoded data. This is done to offer direct compatibility with Foundation strings and `NSRange`, which both use UTF-16.
57 |
58 | Also, to help with all the back and forth, SwiftTreeSitter includes some accessors that are NSRange-based, as well as extension on `NSRange`. These **must** be used when working with the native tree-sitter types unless you take care to handle encoding yourself.
59 |
60 | To keep things clear, consistent naming and types are used. `Node.byteRange` returns a `Range`, which is an encoding-dependent value. `Node.range` is an `NSRange` which is defined to use UTF-16.
61 |
62 | ```swift
63 | let node = tree.rootNode!
64 |
65 | // this is encoding-dependent and cannot be used with your storage
66 | node.byteRange
67 |
68 | // this is a UTF-16-assumed translation of the byte ranges
69 | node.range
70 |
71 | // converting UTF-16-based changed ranges on re-parse
72 | let ranges: [NSRange] = newtree.changedRanges(from: oldTree)
73 | .map{ $0.bytes.range }
74 | ```
75 |
76 | ## Query Conflicts
77 |
78 | SwiftTreeSitter does its best to resolve poor/incorrect query constructs, which are surprisingly common.
79 |
80 | When using injections, child query ranges are automatically expanded using parent matches. This handles cases where a parent has queries that overlap with children in conflicting ways. Without expansion, it is possible to construct queries that fall within children ranges but produce on parent matches.
81 |
82 | All matches are sorted by:
83 |
84 | - depth
85 | - location in content
86 | - specificity of match label (more components => more specific)
87 | - occurrence in the query source
88 |
89 | Even with these, it is possible to produce queries that will result in "incorrect" behavior that are either ambiguous or undefined in the query definition.
90 |
91 | ## Highlighting
92 |
93 | A very common use of tree-sitter is to do syntax highlighting. It is possible to use this library directly, especially if your source text does not change. Here's a little example that sets everything up with a SPM-bundled language.
94 |
95 | First, check out how it works with SwiftTreeSitterLayer. It's complex, but does a lot for you.
96 |
97 | ````swift
98 | // LanguageConfiguration takes care of finding and loading queries in SPM-created bundles.
99 | let markdownConfig = try LanguageConfiguration(tree_sitter_markdown(), name: "Markdown")
100 | let markdownInlineConfig = try LanguageConfiguration(
101 | tree_sitter_markdown_inline(),
102 | name: "MarkdownInline",
103 | bundleName: "TreeSitterMarkdown_TreeSitterMarkdownInline"
104 | )
105 | let swiftConfig = try LanguageConfiguration(tree_sitter_swift(), name: "Swift")
106 |
107 | // Unfortunately, injections do not use standardized language names, and can even be content-dependent. Your system must do this mapping.
108 | let config = LanguageLayer.Configuration(
109 | languageProvider: {
110 | name in
111 | switch name {
112 | case "markdown":
113 | return markdownConfig
114 | case "markdown_inline":
115 | return markdownInlineConfig
116 | case "swift":
117 | return swiftConfig
118 | default:
119 | return nil
120 | }
121 | }
122 | )
123 |
124 | let rootLayer = try LanguageLayer(languageConfig: markdownConfig, configuration: config)
125 |
126 | let source = """
127 | # this is markdown
128 |
129 | ```swift
130 | func main(a: Int) {
131 | }
132 | ```
133 |
134 | ## also markdown
135 |
136 | ```swift
137 | let value = "abc"
138 | ```
139 | """
140 |
141 | rootLayer.replaceContent(with: source)
142 |
143 | let fullRange = NSRange(source.startIndex.. LanguageConfiguration?
12 |
13 | public struct Content {
14 | public let readHandler: Parser.ReadBlock
15 | public let textProvider: SwiftTreeSitter.Predicate.TextProvider
16 |
17 | public init(
18 | readHandler: @escaping Parser.ReadBlock,
19 | textProvider: @escaping SwiftTreeSitter.Predicate.TextProvider
20 | ) {
21 | self.readHandler = readHandler
22 | self.textProvider = textProvider
23 | }
24 |
25 | public init(string: String) {
26 | self.init(string: string, limit: string.utf16.count)
27 | }
28 |
29 | public init(string: String, limit: Int) {
30 | let read = Parser.readFunction(for: string, limit: limit)
31 |
32 | self.init(
33 | readHandler: read,
34 | textProvider: string.predicateTextProvider
35 | )
36 | }
37 | }
38 |
39 | public struct ContentSnapshot: Sendable {
40 | public let readHandler: Parser.DataSnapshotProvider
41 | public let textProvider: SwiftTreeSitter.Predicate.TextSnapshotProvider
42 |
43 | public init(
44 | readHandler: @escaping @Sendable (Int, Point) -> Data?,
45 | textProvider: @escaping @Sendable (NSRange, Range) -> String?
46 | ) {
47 | self.readHandler = readHandler
48 | self.textProvider = textProvider
49 | }
50 |
51 | public init(string: String, limit: Int) {
52 | let read = Parser.readFunction(for: string, limit: limit)
53 |
54 | self.init(
55 | readHandler: read,
56 | textProvider: string.predicateTextSnapshotProvider
57 | )
58 | }
59 |
60 | public init(string: String) {
61 | self.init(string: string, limit: string.utf16.count)
62 | }
63 |
64 | public var content: LanguageLayer.Content {
65 | .init(readHandler: readHandler, textProvider: textProvider)
66 | }
67 | }
68 |
69 | public struct Configuration {
70 | public let languageProvider: LanguageProvider
71 | public let maximumLanguageDepth: Int
72 |
73 | public init(
74 | maximumLanguageDepth: Int = 4,
75 | languageProvider: @escaping LanguageProvider = { _ in nil }
76 | ) {
77 | self.languageProvider = languageProvider
78 | self.maximumLanguageDepth = maximumLanguageDepth
79 | }
80 | }
81 |
82 | private enum NestedEntry {
83 | case layer(LanguageLayer)
84 | case missing(String, [TSRange])
85 | }
86 |
87 | public let languageConfig: LanguageConfiguration
88 | public let depth: Int
89 | private let configuration: Configuration
90 | private let parser = Parser()
91 | private(set) var state = ParseState()
92 | private var sublayers = [String : LanguageLayer]()
93 | private var missingInjections = [String : [TSRange]]()
94 | private let rangeRestricted: Bool
95 |
96 | init(languageConfig: LanguageConfiguration, configuration: Configuration, ranges: [TSRange], depth: Int) throws {
97 | self.languageConfig = languageConfig
98 | self.configuration = configuration
99 | self.rangeRestricted = ranges.isEmpty == false
100 | self.depth = depth
101 |
102 | try parser.setLanguage(languageConfig.language)
103 |
104 | if rangeRestricted {
105 | parser.includedRanges = ranges
106 | }
107 | }
108 |
109 | public convenience init(languageConfig: LanguageConfiguration, configuration: Configuration, depth: Int = 0) throws {
110 | try self.init(languageConfig: languageConfig, configuration: configuration, ranges: [], depth: depth)
111 | }
112 |
113 | public var languageName: String {
114 | languageConfig.name
115 | }
116 |
117 | public var supportsNestedLanguages: Bool {
118 | languageConfig.queries[.injections] != nil && configuration.maximumLanguageDepth > 0
119 | }
120 |
121 | public var includedRangeSet: IndexSet? {
122 | state.includedSet
123 | }
124 | }
125 |
126 | extension LanguageLayer {
127 | func contains(_ range: NSRange) -> Bool {
128 | guard let set = includedRangeSet else {
129 | return false
130 | }
131 |
132 | return set.intersects(integersIn: Range(range)!)
133 | }
134 |
135 | func languageLayer(for range: NSRange) -> LanguageLayer? {
136 | guard contains(range) else {
137 | return nil
138 | }
139 |
140 | return sublayers.values.first(where: { $0.contains(range) }) ?? self
141 | }
142 | }
143 |
144 | extension LanguageLayer {
145 | private func applyEdit(_ edit: InputEdit) {
146 | state.applyEdit(edit)
147 |
148 | // and now update the included ranges
149 | if rangeRestricted, let tree = state.tree {
150 | parser.includedRanges = tree.includedRanges
151 | }
152 |
153 | for sublayer in sublayers.values {
154 | sublayer.applyEdit(edit)
155 | }
156 | }
157 |
158 | private func parse(with content: Content) -> IndexSet {
159 | let newState = parser.parse(state: state, readHandler: content.readHandler)
160 |
161 | let oldState = state
162 |
163 | self.state = newState
164 |
165 | var invalidations = oldState.changedSet(for: newState)
166 |
167 | for layer in sublayers.values {
168 | let subset = layer.parse(with: content)
169 |
170 | invalidations.formUnion(subset)
171 | }
172 |
173 | return invalidations
174 | }
175 |
176 | private func parse(with content: Content, affecting affectedSet: IndexSet, resolveSublayers resolve: Bool) -> IndexSet {
177 | // afer this completes, affectedSet is valid again
178 | var set = parse(with: content)
179 |
180 | set.formUnion(affectedSet)
181 |
182 | if resolve {
183 | do {
184 | let subset = try resolveSublayers(with: content, in: set)
185 |
186 | set.formUnion(subset)
187 | } catch {
188 | print("parsing sublayers for \(languageName) failed: ", error)
189 | }
190 | }
191 |
192 | return set
193 | }
194 |
195 | @discardableResult
196 | public func replaceContent(with string: String, transformer: Point.LocationTransformer = { _ in nil }) -> IndexSet {
197 | let set = includedRangeSet
198 |
199 | let start = set?.first ?? 0
200 | let end = set?.last ?? start
201 |
202 | let fullRange = NSRange(start.. IndexSet {
224 | // includedRangeSet becomes invalid here
225 | applyEdit(edit)
226 |
227 | let editedRange = (edit.startByte.. IndexSet {
234 | var invalidated = IndexSet()
235 |
236 | for sublayer in sublayers.values {
237 | let subset = try sublayer.languageConfigurationChanged(for: name, content: content)
238 |
239 | invalidated.formUnion(subset)
240 | }
241 |
242 | invalidated.formUnion(try fillMissingSublayer(for: name, content: content))
243 |
244 | return invalidated
245 | }
246 | }
247 |
248 | extension LanguageLayer {
249 | public func snapshot(in set: IndexSet? = nil) -> LanguageLayerTreeSnapshot? {
250 | guard let rootSnapshot = LanguageLayerSnapshot(languageLayer: self) else {
251 | return nil
252 | }
253 |
254 | let subSnapshots = sublayers.values.compactMap { $0.snapshot(in: set) }
255 |
256 | if subSnapshots.count != sublayers.count {
257 | return nil
258 | }
259 |
260 | return LanguageLayerTreeSnapshot(rootSnapshot: rootSnapshot, sublayerSnapshots: subSnapshots)
261 | }
262 | }
263 |
264 | extension LanguageLayer: Queryable {
265 | private func executeShallowQuery(_ queryDef: Query.Definition, in set: IndexSet) throws -> LanguageLayerQueryCursor {
266 | let name = languageConfig.name
267 |
268 | guard let query = languageConfig.queries[queryDef] else {
269 | throw LanguageLayerError.queryUnavailable(name, queryDef)
270 | }
271 |
272 | // a copy here is a small inefficiency...
273 | guard let tree = state.tree?.copy() else {
274 | throw LanguageLayerError.noRootNode
275 | }
276 |
277 | let target = LanguageLayerQueryCursor.Target(tree: tree, query: query, depth: depth, name: languageName)
278 |
279 | return LanguageLayerQueryCursor(target: target, set: set)
280 | }
281 |
282 | public func executeQuery(_ queryDef: Query.Definition, in set: IndexSet) throws -> LanguageTreeQueryCursor {
283 | guard let treeSnapshot = snapshot(in: set) else {
284 | throw LanguageLayerError.noRootNode
285 | }
286 |
287 | return try treeSnapshot.executeQuery(queryDef, in: set)
288 | }
289 | }
290 |
291 | extension LanguageLayer {
292 | private func fillMissingSublayer(for name: String, content: Content) throws -> IndexSet {
293 | guard let tsRanges = missingInjections[name] else {
294 | return IndexSet()
295 | }
296 |
297 | return try addNewSublayer(named: name, tsRanges: tsRanges, content: content)
298 | }
299 |
300 | private func addNewSublayer(named name: String, tsRanges: [TSRange], content: Content) throws -> IndexSet {
301 | precondition(sublayers[name] == nil)
302 |
303 | guard let subLang = configuration.languageProvider(name) else {
304 | self.missingInjections[name] = tsRanges
305 |
306 | return IndexSet()
307 | }
308 |
309 | let subConfig = Configuration(
310 | maximumLanguageDepth: max(0, configuration.maximumLanguageDepth - 1),
311 | languageProvider: configuration.languageProvider
312 | )
313 |
314 | let layer = try LanguageLayer(languageConfig: subLang, configuration: subConfig, ranges: tsRanges, depth: depth + 1)
315 |
316 | self.sublayers[name] = layer
317 | self.missingInjections[name] = nil
318 |
319 | var affectedSet = IndexSet()
320 |
321 | for tsRange in tsRanges {
322 | let rangeSet = IndexSet(integersIn: tsRange.bytes.range)
323 |
324 | affectedSet.formUnion(rangeSet)
325 | }
326 |
327 | return layer.parse(with: content, affecting: affectedSet, resolveSublayers: true)
328 | }
329 |
330 | private func encorporateRanges(_ tsRanges: [TSRange], content: Content) throws -> IndexSet {
331 | guard let includedRanges = state.tree?.includedRanges else {
332 | preconditionFailure("Cannot encorporateRanges into a layer that doesn't have any already defined")
333 | }
334 |
335 | var allRanges = includedRanges
336 | var invalidation = IndexSet()
337 |
338 | for newTSRange in tsRanges {
339 | allRanges.removeAll(where: { newTSRange.bytes.lowerBound == $0.bytes.lowerBound })
340 |
341 | allRanges.append(newTSRange)
342 |
343 | invalidation.insert(integersIn: Range(newTSRange.bytes.range)!)
344 | }
345 |
346 | // included ranges must be sorted and the above algorithm does not guarantee that
347 | self.parser.includedRanges = allRanges.sorted()
348 |
349 | let set = parse(with: content)
350 |
351 | invalidation.formUnion(set)
352 |
353 | return invalidation
354 | }
355 |
356 | /// Recursively resolve any language injections within the set.
357 | ///
358 | /// This process is manual to offer the greatest control to clients.
359 | public func resolveSublayers(with content: LanguageLayer.Content, in set: IndexSet) throws -> IndexSet {
360 | guard supportsNestedLanguages else {
361 | return IndexSet()
362 | }
363 |
364 | // injections must be shallow
365 | let injections = try executeShallowQuery(.injections, in: set).injections(with: content.textProvider)
366 | let groupedInjections = Dictionary(grouping: injections, by: { $0.name })
367 | var invalidations = IndexSet()
368 |
369 | // they could be new, or could be updates to existing
370 | for pair in groupedInjections {
371 | let name = pair.0
372 | let ranges = pair.value.map { $0.tsRange }
373 |
374 | guard let sublayer = sublayers[name] else {
375 | let set = try addNewSublayer(named: name, tsRanges: ranges, content: content)
376 |
377 | invalidations.formUnion(set)
378 |
379 | continue
380 | }
381 |
382 | let set = try sublayer.encorporateRanges(ranges, content: content)
383 |
384 | invalidations.formUnion(set)
385 | }
386 |
387 | return invalidations
388 | }
389 | }
390 |
--------------------------------------------------------------------------------
/Sources/SwiftTreeSitter/Query.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import TreeSitter
3 |
4 | public enum QueryError: Error {
5 | case none
6 | case syntax(UInt32)
7 | case nodeType(UInt32)
8 | case field(UInt32)
9 | case capture(UInt32)
10 | case structure(UInt32)
11 | case unknown(UInt32)
12 |
13 | init(offset: UInt32, internalError: TSQueryError) {
14 | switch internalError {
15 | case TSQueryErrorNone:
16 | self = .none
17 | case TSQueryErrorSyntax:
18 | self = .syntax(offset)
19 | case TSQueryErrorNodeType:
20 | self = .nodeType(offset)
21 | case TSQueryErrorField:
22 | self = .field(offset)
23 | case TSQueryErrorCapture:
24 | self = .capture(offset)
25 | case TSQueryErrorStructure:
26 | self = .structure(offset)
27 | default:
28 | self = .unknown(offset)
29 | }
30 | }
31 | }
32 |
33 | public enum QueryPredicateError: Error {
34 | case valueNotFound
35 | case unrecognizedStepType
36 | case queryInvalid
37 | case textContentUnavailable
38 | }
39 |
40 | /// An object that represents a collection of tree-sitter query statements.
41 | ///
42 | /// Typically, query definitions are stored in a `.scm` file.
43 | ///
44 | /// Tree-sitter's official documentation: [Pattern Matching with Queries](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries)
45 | public final class Query: Sendable {
46 | let internalQueryPointer: SendableOpaquePointer
47 | let predicateList: [[Predicate]]
48 |
49 | /// Construct a query object from scm data
50 | ///
51 | /// This operation has do to a lot of work, especially if any
52 | /// patterns contain predicates. You should expect it will
53 | /// be expensive.
54 | public init(language: Language, data: Data) throws {
55 | let dataLength = data.count
56 | var errorOffset: UInt32 = 0
57 | var queryError: TSQueryError = TSQueryErrorNone
58 |
59 | let result = data.withUnsafeBytes { byteBuffer -> OpaquePointer? in
60 | guard let ptr = byteBuffer.baseAddress?.bindMemory(to: CChar.self, capacity: dataLength) else {
61 | return nil
62 | }
63 |
64 | return ts_query_new(language.tsLanguage,
65 | ptr,
66 | UInt32(dataLength),
67 | &errorOffset,
68 | &queryError)
69 | }
70 |
71 | guard let queryPtr = result else {
72 | throw QueryError(offset: errorOffset, internalError: queryError)
73 | }
74 |
75 | self.internalQueryPointer = SendableOpaquePointer(queryPtr)
76 | self.predicateList = try PredicateParser().predicates(in: queryPtr)
77 | }
78 |
79 | /// Construct a query object from scm data located at a url
80 | public convenience init(language: Language, url: URL) throws {
81 | try self.init(language: language, data: try Data(contentsOf: url))
82 | }
83 |
84 | deinit {
85 | ts_query_delete(internalQuery)
86 | }
87 |
88 | var internalQuery: OpaquePointer {
89 | internalQueryPointer.pointer
90 | }
91 |
92 | public var patternCount: Int {
93 | return Int(ts_query_pattern_count(internalQuery))
94 | }
95 |
96 | public var captureCount: Int {
97 | return Int(ts_query_capture_count(internalQuery))
98 | }
99 |
100 | public var stringCount: Int {
101 | return Int(ts_query_string_count(internalQuery))
102 | }
103 |
104 | /// Run a query
105 | ///
106 | /// - Parameter node: the root node for the query
107 | /// - Parameter tree: a reference to the tree
108 | /// - Parameter depth: the language injection depth
109 | public func execute(node: Node, in tree: Tree, depth: Int = 0) -> QueryCursor {
110 | let cursor = QueryCursor(internalTree: tree, depth: depth)
111 |
112 | cursor.execute(query: self, node: node)
113 |
114 | return cursor
115 | }
116 |
117 | public func execute(node: Node, in tree: MutableTree) -> QueryCursor {
118 | execute(node: node, in: tree.tree)
119 | }
120 |
121 | /// Run a query against the root node of a tree.
122 | ///
123 | /// - Parameter tree: a reference to the tree
124 | /// - Parameter depth: the language injection depth
125 | public func execute(in tree: Tree, depth: Int = 0) -> QueryCursor {
126 | let cursor = QueryCursor(internalTree: tree, depth: depth)
127 |
128 | if let node = tree.rootNode {
129 | cursor.execute(query: self, node: node)
130 | }
131 |
132 | return cursor
133 | }
134 |
135 | public func execute(in tree: MutableTree) -> QueryCursor {
136 | execute(in: tree.tree)
137 | }
138 |
139 | public func captureName(for id: Int) -> String? {
140 | var length: UInt32 = 0
141 |
142 | guard let cStr = ts_query_capture_name_for_id(internalQuery, UInt32(id), &length) else {
143 | return nil
144 | }
145 |
146 | return String(cString: cStr)
147 | }
148 |
149 | public func stringName(for id: Int) -> String? {
150 | var length: UInt32 = 0
151 |
152 | guard let cStr = ts_query_string_value_for_id(internalQuery, UInt32(id), &length) else {
153 | return nil
154 | }
155 |
156 | return String(cString: cStr)
157 | }
158 |
159 | public func predicates(for patternIndex: Int) -> [Predicate] {
160 | return predicateList[patternIndex]
161 | }
162 |
163 | public var hasPredicates: Bool {
164 | for i in 0.. [String: String] {
208 | let pairs = predicates.compactMap { predicate -> (String, String)? in
209 | switch predicate {
210 | case let .set(captureName: captureName, key: key, value: value):
211 | if captureName == name {
212 | return (key, value)
213 | }
214 | default:
215 | break
216 | }
217 |
218 | return nil
219 | }
220 |
221 | return Dictionary(pairs, uniquingKeysWith: { $1 })
222 | }
223 |
224 | public var range: NSRange {
225 | return node.range
226 | }
227 |
228 | public var name: String? {
229 | return nameComponents.joined(separator: ".")
230 | }
231 | }
232 |
233 | extension QueryCapture: CustomDebugStringConvertible {
234 | public var debugDescription: String {
235 | let name = name ?? ""
236 |
237 | return ""
238 | }
239 | }
240 |
241 | extension QueryCapture: Comparable {
242 | public static func < (lhs: QueryCapture, rhs: QueryCapture) -> Bool {
243 | if lhs.depth != rhs.depth {
244 | return lhs.depth < rhs.depth
245 | }
246 |
247 | if lhs.range.lowerBound != rhs.range.lowerBound {
248 | return lhs.range.lowerBound < rhs.range.lowerBound
249 | }
250 |
251 | if lhs.nameComponents.count != rhs.nameComponents.count {
252 | return lhs.nameComponents.count < rhs.nameComponents.count
253 | }
254 |
255 | return lhs.patternIndex < rhs.patternIndex
256 | }
257 | }
258 |
259 | public struct QueryMatch {
260 | public var id: Int
261 | public var patternIndex: Int
262 | public var captures: [QueryCapture]
263 | public let predicates: [Predicate]
264 | public let metadata: [String: String]
265 |
266 | public func captures(named name: String) -> [QueryCapture] {
267 | return captures.filter({ $0.name == name })
268 | }
269 |
270 | /// Returns all nodes that correspond to the captures.
271 | public var nodes: [Node] {
272 | captures.map { $0.node }
273 | }
274 |
275 | /// Returns a range that spans all captures.
276 | public var range: NSRange? {
277 | guard let first = captures.first else {
278 | return nil
279 | }
280 |
281 | return captures.dropFirst().reduce(first.range, { $0.union($1.range) })
282 | }
283 |
284 | /// Determines if this match is actually allowed given the context.
285 | public func allowed(in context: Predicate.Context) -> Bool {
286 | predicates.allSatisfy { $0.allowsMatch(self, context: context) }
287 | }
288 | }
289 |
290 | /// A tree-sitter TSQueryCursor wrapper
291 | ///
292 | /// This class is pretty faithful to to C API. However, it does evaluate `#set!` directives.
293 | public final class QueryCursor {
294 | let internalCursor: OpaquePointer
295 | let internalTree: Tree
296 | let depth: Int
297 |
298 | public private(set) var activeQuery: Query?
299 |
300 | init(internalTree: Tree, depth: Int) {
301 | self.internalCursor = ts_query_cursor_new()
302 | self.internalTree = internalTree
303 | self.depth = depth
304 | }
305 |
306 | deinit {
307 | ts_query_cursor_delete(internalCursor)
308 | }
309 |
310 | /// Run a query
311 | ///
312 | /// Note that the node **and** the Tree is is part of
313 | /// must remain valid as long as the query is being used.
314 | ///
315 | /// - Parameter query: the query object to execute
316 | /// - Parameter node: the root node for the query
317 | public func execute(query: Query, node: Node) {
318 | self.activeQuery = query
319 |
320 | ts_query_cursor_exec(internalCursor, query.internalQuery, node.internalNode)
321 | }
322 |
323 | public var matchLimit: Int {
324 | get {
325 | Int(ts_query_cursor_match_limit(internalCursor))
326 | }
327 | set {
328 | ts_query_cursor_set_match_limit(internalCursor, UInt32(newValue))
329 | }
330 | }
331 |
332 | public func setByteRange(range: Range) {
333 | ts_query_cursor_set_byte_range(internalCursor, range.lowerBound, range.upperBound)
334 | }
335 |
336 | public func setRange(_ range: NSRange) {
337 | setByteRange(range: range.byteRange)
338 | }
339 |
340 | public func setPointRange(range: Range) {
341 | let start = range.lowerBound.internalPoint
342 | let end = range.upperBound.internalPoint
343 |
344 | ts_query_cursor_set_point_range(internalCursor, start, end)
345 | }
346 |
347 | @available(*, deprecated, renamed: "next")
348 | public func nextMatch() -> QueryMatch? {
349 | return next()
350 | }
351 |
352 | public func nextCapture() -> QueryCapture? {
353 | var match = TSQueryMatch(id: 0, pattern_index: 0, capture_count: 0, captures: nil)
354 | var index: UInt32 = 0
355 |
356 | if ts_query_cursor_next_capture(internalCursor, &match, &index) == false {
357 | return nil
358 | }
359 |
360 | let captureBuffer = UnsafeBufferPointer(start: match.captures,
361 | count: Int(match.capture_count))
362 |
363 | let capture = captureBuffer[Int(index)]
364 |
365 | return QueryCapture(
366 | tsCapture: capture,
367 | internalTree: internalTree,
368 | query: activeQuery,
369 | patternIndex: Int(match.pattern_index),
370 | depth: depth
371 | )
372 | }
373 | }
374 |
375 | extension QueryCursor: Sequence, IteratorProtocol {
376 | private func evaluateDirectives(_ predicates: [Predicate]) -> [String: String] {
377 | let pairs = predicates.compactMap { predicate -> (String, String)? in
378 | switch predicate {
379 | case .set(captureName: nil, key: let key, value: let value):
380 | return (key, value)
381 | default:
382 | return nil
383 | }
384 | }
385 |
386 | return Dictionary(pairs, uniquingKeysWith: { $1 })
387 | }
388 |
389 | public func next() -> QueryMatch? {
390 | var match = TSQueryMatch(id: 0, pattern_index: 0, capture_count: 0, captures: nil)
391 |
392 | if ts_query_cursor_next_match(internalCursor, &match) == false {
393 | return nil
394 | }
395 |
396 | let captureBuffer = UnsafeBufferPointer(start: match.captures,
397 | count: Int(match.capture_count))
398 |
399 | let patternIndex = Int(match.pattern_index)
400 | let predicates = activeQuery?.predicates(for: patternIndex) ?? []
401 | let metadata = evaluateDirectives(predicates)
402 |
403 | let captures = captureBuffer.compactMap({
404 | return QueryCapture(tsCapture: $0, internalTree: internalTree, query: activeQuery, patternIndex: patternIndex, depth: depth)
405 | })
406 |
407 | return QueryMatch(id: Int(match.id),
408 | patternIndex: Int(match.pattern_index),
409 | captures: captures,
410 | predicates: predicates,
411 | metadata: metadata)
412 | }
413 | }
414 |
--------------------------------------------------------------------------------