├── .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 | --------------------------------------------------------------------------------