Skip to content

Commit ba5a9c2

Browse files
committed
Add tree sitter as AST parser
1 parent adeeba3 commit ba5a9c2

7 files changed

Lines changed: 201 additions & 0 deletions

File tree

Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
import SwiftUI
3+
import AppKit
4+
import ASTParser
5+
import PlaygroundSupport
6+
7+
struct ParsingForm: View {
8+
@State var filePath: String = ""
9+
@State var result: String = ""
10+
11+
var body: some View {
12+
Form {
13+
Section("Input") {
14+
TextField("File Path", text: $filePath)
15+
Button("Parse") {
16+
result = ""
17+
Task {
18+
let fileContent = try String(contentsOfFile: filePath)
19+
let parser = ASTParser(language: .swift)
20+
let tree = parser.parse(fileContent)
21+
result = tree?.dump() ?? "N/A"
22+
}
23+
}
24+
}
25+
26+
Section("Result") {
27+
Text(result)
28+
.fontDesign(.monospaced)
29+
}
30+
}
31+
.formStyle(.grouped)
32+
.frame(width: 600, height: 800)
33+
}
34+
}
35+
36+
PlaygroundPage.current.needsIndefiniteExecution = true
37+
PlaygroundPage.current.setLiveView(NSHostingController(rootView: ParsingForm()))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Timeline
3+
version = "3.0">
4+
<TimelineItems>
5+
</TimelineItems>
6+
<TimelineItems>
7+
</TimelineItems>
8+
</Timeline>

Playground.playground/contents.xcplayground

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
<pages>
44
<page name='RetrievalQAChain'/>
55
<page name='WebScrapper'/>
6+
<page name='ASTParsing'/>
67
</pages>
78
</playground>

Tool/Package.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let package = Package(
1616
.library(name: "ChatTab", targets: ["ChatTab"]),
1717
.library(name: "Environment", targets: ["Environment"]),
1818
.library(name: "SuggestionModel", targets: ["SuggestionModel"]),
19+
.library(name: "ASTParser", targets: ["ASTParser"]),
1920
.library(name: "Toast", targets: ["Toast"]),
2021
.library(name: "Keychain", targets: ["Keychain"]),
2122
.library(name: "SharedUIComponents", targets: ["SharedUIComponents"]),
@@ -44,6 +45,14 @@ let package = Package(
4445
url: "https://github.com/pointfreeco/swift-composable-architecture",
4546
from: "0.55.0"
4647
),
48+
49+
// TreeSitter
50+
.package(url: "https://github.com/ChimeHQ/SwiftTreeSitter", from: "0.7.1"),
51+
.package(
52+
url: "https://github.com/alex-pinkus/tree-sitter-swift",
53+
branch: "with-generated-files"
54+
),
55+
.package(url: "https://github.com/lukepistrol/tree-sitter-objc", branch: "feature/spm"),
4756
],
4857
targets: [
4958
// MARK: - Helpers
@@ -142,6 +151,12 @@ let package = Package(
142151
),
143152
.testTarget(name: "SharedUIComponentsTests", dependencies: ["SharedUIComponents"]),
144153

154+
.target(name: "ASTParser", dependencies: [
155+
.product(name: "SwiftTreeSitter", package: "SwiftTreeSitter"),
156+
.product(name: "TreeSitterObjC", package: "tree-sitter-objc"),
157+
.product(name: "TreeSitterSwift", package: "tree-sitter-swift"),
158+
]),
159+
145160
// MARK: - Services
146161

147162
.target(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import SwiftTreeSitter
2+
import tree_sitter
3+
import TreeSitterObjC
4+
import TreeSitterSwift
5+
6+
public enum ParsableLanguage {
7+
case swift
8+
case objectiveC
9+
10+
var tsLanguage: UnsafeMutablePointer<TSLanguage> {
11+
switch self {
12+
case .swift:
13+
return tree_sitter_swift()
14+
case .objectiveC:
15+
return tree_sitter_objc()
16+
}
17+
}
18+
}
19+
20+
public struct ASTParser {
21+
let language: ParsableLanguage
22+
let parser: Parser
23+
24+
public init(language: ParsableLanguage) {
25+
self.language = language
26+
parser = Parser()
27+
try! parser.setLanguage(Language(language: language.tsLanguage))
28+
}
29+
30+
public func parse(_ source: String) -> ASTTree? {
31+
return ASTTree(tree: parser.parse(source))
32+
}
33+
}
34+
35+
public struct ASTTree {
36+
public let tree: Tree?
37+
38+
public var rootNode: Node? {
39+
return tree?.rootNode
40+
}
41+
}
42+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import SwiftTreeSitter
2+
3+
public extension ASTTree {
4+
/// Dumps the syntax tree as a string, for debugging purposes.
5+
func dump() -> String {
6+
guard let tree, let root = tree.rootNode else { return "" }
7+
var result = ""
8+
9+
let appendNode: (_ level: Int, _ node: Node) -> Void = { level, node in
10+
let line =
11+
"\(String(repeating: " ", count: level))\(node.nodeType ?? "N/A") \(node.pointRange)"
12+
result += line + "\n"
13+
}
14+
15+
guard let node = root.descendant(in: root.byteRange) else { return result }
16+
17+
appendNode(0, node)
18+
19+
let cursor = node.treeCursor
20+
let level = 1
21+
22+
if cursor.goToFirstChild(for: node.byteRange.lowerBound) == false {
23+
return result
24+
}
25+
26+
cursor.enumerateCurrentAndDescendents(level: level + 1) { level, node in
27+
appendNode(level, node)
28+
}
29+
30+
while cursor.gotoNextSibling() {
31+
guard let node = cursor.currentNode else {
32+
assertionFailure("no current node when gotoNextSibling succeeded")
33+
break
34+
}
35+
36+
// once we are past the interesting range, stop
37+
if node.byteRange.lowerBound > root.byteRange.upperBound {
38+
break
39+
}
40+
41+
cursor.enumerateCurrentAndDescendents(level: level + 1) { level, node in
42+
appendNode(level, node)
43+
}
44+
}
45+
46+
return result
47+
}
48+
}
49+
50+
private extension TreeCursor {
51+
func enumerateCurrentAndDescendents(level: Int, block: (Int, Node) throws -> Void) rethrows {
52+
if let node = currentNode {
53+
try block(level, node)
54+
}
55+
56+
if goToFirstChild() == false {
57+
return
58+
}
59+
60+
try enumerateCurrentAndDescendents(level: level + 1, block: block)
61+
62+
while gotoNextSibling() {
63+
try enumerateCurrentAndDescendents(level: level + 1, block: block)
64+
}
65+
66+
let success = gotoParent()
67+
68+
assert(success)
69+
}
70+
}
71+

0 commit comments

Comments
 (0)