-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSearchCommand.swift
More file actions
121 lines (103 loc) · 4.53 KB
/
Copy pathSearchCommand.swift
File metadata and controls
121 lines (103 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import ArgumentParser
import Foundation
import SyntaxSparrow
import SwiftSyntax
// MARK: - search subcommand (project-wide clip search)
struct SearchCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "search",
abstract: "Find declarations matching a pattern across a directory"
)
@Argument(help: "Substring to match against signatures")
var pattern: String
@Argument(help: "Directory to scan (default: current directory)")
var directory: String = "."
@Flag(help: "Emit JSON")
var json: Bool = false
@Flag(help: "Copy the first match body to the system pasteboard")
var pboard: Bool = false
@Option(help: "Limit results (0 = unlimited)")
var limit: Int = 0
func run() throws {
let expanded = (directory as NSString).expandingTildeInPath
let swiftFiles = try collectSwiftFiles(in: expanded)
struct Match {
let file: String
let signature: String
let body: String
}
var matches: [Match] = []
outer: for path in swiftFiles {
guard let source = try? String(contentsOfFile: path, encoding: .utf8) else { continue }
let doc = SyntaxTree(viewMode: .fixedUp, sourceBuffer: source)
doc.collectChildren()
// Check structures and their members
for structure in doc.structures {
for v in structure.variables {
let sig = v.code()
if sig.localizedCaseInsensitiveContains(pattern) {
let body = String(source[source.indexRange(for: v.node) ?? source.startIndex..<source.startIndex])
matches.append(Match(file: path, signature: "\(structure.name).\(sig)", body: body))
if limit > 0 && matches.count >= limit { break outer }
}
}
for fn in structure.functions {
let sig = fn.code()
if sig.localizedCaseInsensitiveContains(pattern) {
let body = String(source[source.indexRange(for: fn.node) ?? source.startIndex..<source.startIndex])
matches.append(Match(file: path, signature: "\(structure.name).\(sig)", body: body))
if limit > 0 && matches.count >= limit { break outer }
}
}
}
// Top-level functions
for fn in doc.functions {
let sig = fn.code()
if sig.localizedCaseInsensitiveContains(pattern) {
let body = String(source[source.indexRange(for: fn.node) ?? source.startIndex..<source.startIndex])
matches.append(Match(file: path, signature: sig, body: body))
if limit > 0 && matches.count >= limit { break outer }
}
}
}
if matches.isEmpty {
fputs("No declarations matching '\(pattern)' found in \(expanded)\n", stderr)
throw ExitCode.failure
}
if json {
let out = matches.map { m -> [String: Any] in
["file": relativePath(m.file, base: expanded), "signature": m.signature]
}
if let data = try? JSONSerialization.data(withJSONObject: out, options: .prettyPrinted),
let str = String(data: data, encoding: .utf8) {
print(str)
}
return
}
// Plain output: file:signature
for m in matches {
print("\(relativePath(m.file, base: expanded)): \(m.signature)")
}
// Optionally copy first match body to pasteboard
if pboard, let first = matches.first {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/pbcopy")
let pipe = Pipe()
process.standardInput = pipe
try process.run()
pipe.fileHandleForWriting.write(Data(first.body.utf8))
pipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
fputs("copied '\(first.signature)' to pasteboard\n", stderr)
}
}
private func relativePath(_ path: String, base: String) -> String {
path.hasPrefix(base) ? String(path.dropFirst(base.count + 1)) : path
}
}
// Helper to get a node's source range as String.Index range
private extension String {
func indexRange(for node: some SyntaxProtocol) -> Range<Index>? {
indexRange(from: node.position, to: node.endPosition)
}
}