forked from github/CopilotForXcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWorkspaceFile.swift
More file actions
180 lines (153 loc) · 6.99 KB
/
WorkspaceFile.swift
File metadata and controls
180 lines (153 loc) · 6.99 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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import Foundation
import Logger
import ConversationServiceProvider
public let supportedFileExtensions: Set<String> = ["swift", "m", "mm", "h", "cpp", "c", "js", "py", "rb", "java", "applescript", "scpt", "plist", "entitlements", "md", "json", "xml", "txt", "yaml", "yml"]
public let skipPatterns: [String] = [
".git",
".svn",
".hg",
"CVS",
".DS_Store",
"Thumbs.db",
"node_modules",
"bower_components"
]
public struct WorkspaceFile {
static func isXCWorkspace(_ url: URL) -> Bool {
return url.pathExtension == "xcworkspace" && FileManager.default.fileExists(atPath: url.appendingPathComponent("contents.xcworkspacedata").path)
}
static func isXCProject(_ url: URL) -> Bool {
return url.pathExtension == "xcodeproj" && FileManager.default.fileExists(atPath: url.appendingPathComponent("project.pbxproj").path)
}
static func getWorkspaceByProject(_ url: URL) -> URL? {
guard isXCProject(url) else { return nil }
let workspaceURL = url.appendingPathComponent("project.xcworkspace")
return isXCWorkspace(workspaceURL) ? workspaceURL : nil
}
static func getSubprojectURLs(workspaceURL: URL, data: Data) -> [URL] {
var subprojectURLs: [URL] = []
do {
let xml = try XMLDocument(data: data)
let fileRefs = try xml.nodes(forXPath: "//FileRef")
for fileRef in fileRefs {
if let fileRefElement = fileRef as? XMLElement,
let location = fileRefElement.attribute(forName: "location")?.stringValue {
var path = ""
if location.starts(with: "group:") {
path = location.replacingOccurrences(of: "group:", with: "")
} else if location.starts(with: "container:") {
path = location.replacingOccurrences(of: "container:", with: "")
} else if location.starts(with: "self:") {
// Handle "self:" referece - refers to the containing project directory
var workspaceURLCopy = workspaceURL
workspaceURLCopy.deleteLastPathComponent()
path = workspaceURLCopy.path
} else {
// Skip absolute paths such as absolute:/path/to/project
continue
}
if path.hasSuffix(".xcodeproj") {
path = (path as NSString).deletingLastPathComponent
}
let subprojectURL = path.isEmpty ? workspaceURL.deletingLastPathComponent() : workspaceURL.deletingLastPathComponent().appendingPathComponent(path)
if !subprojectURLs.contains(subprojectURL) {
subprojectURLs.append(subprojectURL)
}
}
}
} catch {
Logger.client.error("Failed to parse workspace file: \(error)")
}
return subprojectURLs
}
static func getSubprojectURLs(in workspaceURL: URL) -> [URL] {
let workspaceFile = workspaceURL.appendingPathComponent("contents.xcworkspacedata")
guard let data = try? Data(contentsOf: workspaceFile) else {
Logger.client.error("Failed to read workspace file at \(workspaceFile.path)")
return []
}
return getSubprojectURLs(workspaceURL: workspaceURL, data: data)
}
static func matchesPatterns(_ url: URL, patterns: [String]) -> Bool {
let fileName = url.lastPathComponent
for pattern in patterns {
if fnmatch(pattern, fileName, 0) == 0 {
return true
}
}
return false
}
public static func getFilesInActiveWorkspace(
workspaceURL: URL,
workspaceRootURL: URL,
shouldExcludeFile: ((URL) -> Bool)? = nil
) -> [FileReference] {
var files: [FileReference] = []
do {
let fileManager = FileManager.default
var subprojects: [URL] = []
if isXCWorkspace(workspaceURL) {
subprojects = getSubprojectURLs(in: workspaceURL)
} else {
subprojects.append(workspaceRootURL)
}
for subproject in subprojects {
guard FileManager.default.fileExists(atPath: subproject.path) else {
continue
}
let enumerator = fileManager.enumerator(
at: subproject,
includingPropertiesForKeys: [.isRegularFileKey, .isDirectoryKey],
options: [.skipsHiddenFiles]
)
while let fileURL = enumerator?.nextObject() as? URL {
// Skip items matching the specified pattern
if matchesPatterns(fileURL, patterns: skipPatterns) || isXCWorkspace(fileURL) ||
isXCProject(fileURL) {
enumerator?.skipDescendants()
continue
}
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey, .isDirectoryKey])
// Handle directories if needed
if resourceValues.isDirectory == true {
continue
}
guard resourceValues.isRegularFile == true else { continue }
if supportedFileExtensions.contains(fileURL.pathExtension.lowercased()) == false {
continue
}
// Apply the custom file exclusion check if provided
if let shouldExcludeFile = shouldExcludeFile,
shouldExcludeFile(fileURL) { continue }
let relativePath = fileURL.path.replacingOccurrences(of: workspaceRootURL.path, with: "")
let fileName = fileURL.lastPathComponent
let file = FileReference(url: fileURL, relativePath: relativePath, fileName: fileName)
files.append(file)
}
}
} catch {
Logger.client.error("Failed to get files in workspace: \(error)")
}
return files
}
/*
used for `project-context` skill. Get filed for watching for syncing to CLS
*/
public static func getWatchedFiles(
workspaceURL: URL,
projectURL: URL,
excludeGitIgnoredFiles: Bool,
excludeIDEIgnoredFiles: Bool
) -> [String] {
// Directly return for invalid workspace
guard workspaceURL.path != "/" else { return [] }
// TODO: implement
let shouldExcludeFile: ((URL) -> Bool)? = nil
let files = getFilesInActiveWorkspace(
workspaceURL: workspaceURL,
workspaceRootURL: projectURL,
shouldExcludeFile: shouldExcludeFile
)
return files.map { $0.url.absoluteString }
}
}