forked from github/CopilotForXcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMultiFileContextManager.swift
More file actions
135 lines (112 loc) · 4.84 KB
/
MultiFileContextManager.swift
File metadata and controls
135 lines (112 loc) · 4.84 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import Foundation
public class MultiFileContextManager {
private let workspaceProvider: WorkspaceProvider
private let parser: ProgrammingLanguageSyntaxParser
public init(workspaceProvider: WorkspaceProvider, parser: ProgrammingLanguageSyntaxParser) {
self.workspaceProvider = workspaceProvider
self.parser = parser
}
/// List files within workspace recursively
/// Retrieved from: https://stackoverflow.com/a/57640445
public func listFilesInWorkspace() async -> [String] {
guard let workspaceURL = try? await workspaceProvider.getProjectRootURL()
else { return [] }
var files = [String]()
if let enumerator = FileManager.default.enumerator(
at: workspaceURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles, .skipsPackageDescendants]
) {
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if fileAttributes.isRegularFile ?? false, fileURL.pathExtension.lowercased() == "swift" {
files.append(fileURL.absoluteString)
}
} catch { print(error, fileURL) }
}
}
return files
}
public func readFileContents() async -> [FileContent] {
let fileURLs = await listFilesInWorkspace()
return fileURLs.compactMap { fileURLString in
guard let fileURL = URL(string: fileURLString) else { return nil }
do {
let content = try String(contentsOf: fileURL, encoding: .utf8)
return FileContent(fileURL: fileURLString, content: content)
} catch {
print("Failed to read \(fileURL):", error)
return nil
}
}
}
public func classifyContentWithinFiles() async -> [String: SymbolContent] {
let fileContents = await readFileContents()
var result: [String: SymbolContent] = [:]
for file in fileContents {
var symbols = parser.parse(file: file)
mergeExtensionsIntoBaseDeclarations(&symbols)
for symbol in symbols {
result[symbol.symbol.name] = symbol
}
}
return result
}
public func retrieveRelevantSymbolsForFileContent(
file: FileContent,
ignoreWithinPaths: [String] = []
) async -> [String: SymbolContent] {
let currentSymbolName = parser.parse(file: file).first?.symbol.name
let allSymbols = await classifyContentWithinFiles()
var relevant: [String: SymbolContent] = [:]
for (name, content) in allSymbols {
if let current = currentSymbolName, current == name { continue }
if ignoreWithinPaths.contains(where: { content.fileURL.contains($0) }) { continue }
if file.content.containsExactIdentifier(name) {
if relevant[name] == nil {
relevant[name] = content
}
}
}
return relevant
}
private func mergeExtensionsIntoBaseDeclarations(_ symbols: inout [SymbolContent]) {
var indexesToRemove: [Int] = []
for (index, symbol) in symbols.enumerated() {
guard symbol.symbol.kind == .extensionWord else { continue }
if let targetIndex = symbols.firstIndex(where: {
$0.symbol.name == symbol.symbol.name &&
$0.symbol.kind != .extensionWord
}) {
var target = symbols[targetIndex]
target.symbol.extensions.append(symbol)
symbols[targetIndex] = target
indexesToRemove.append(index)
}
}
for index in indexesToRemove.sorted(by: >) {
symbols.remove(at: index)
}
}
}
extension String {
/// Allow any character besides letters, numbers, and underscores as boundaries
func containsExactIdentifier(_ name: String) -> Bool {
guard !name.isEmpty else { return false }
@inline(__always)
func isIdentChar(_ c: Character) -> Bool { c.isLetter || c.isNumber || c == "_" }
var search = startIndex..<endIndex
while let range = self.range(of: name, options: .literal, range: search) {
let before = (range.lowerBound == startIndex) ? nil : self[index(before: range.lowerBound)]
let after = (range.upperBound == endIndex) ? nil : self[range.upperBound]
let boundaryBefore = before.map { !isIdentChar($0) } ?? true
let boundaryAfter = after.map { !isIdentChar($0) } ?? true
if boundaryBefore && boundaryAfter {
return true
}
search = range.upperBound..<endIndex
}
return false
}
}