Skip to content

Commit fc587b8

Browse files
committed
review
1 parent d0133ce commit fc587b8

File tree

1 file changed

+51
-71
lines changed

1 file changed

+51
-71
lines changed

Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift

+51-71
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import SwiftUI
55
struct FilePicker: View {
66
@Environment(\.dismiss) var dismiss
77
@StateObject private var model: FilePickerModel
8-
@State private var selection: FilePickerItemModel.ID?
8+
@State private var selection: FilePickerEntryModel?
99

1010
@Binding var outputAbsPath: String
1111

@@ -35,17 +35,16 @@ struct FilePicker: View {
3535
.padding()
3636
} else {
3737
List(selection: $selection) {
38-
ForEach(model.rootFiles) { rootItem in
39-
FilePickerItem(item: rootItem)
38+
ForEach(model.rootEntries) { entry in
39+
FilePickerEntry(entry: entry).tag(entry)
4040
}
4141
}.contextMenu(
42-
forSelectionType: FilePickerItemModel.ID.self,
42+
forSelectionType: FilePickerEntryModel.self,
4343
menu: { _ in },
4444
primaryAction: { selections in
4545
// Per the type of `selection`, this will only ever be a set of
46-
// one item.
47-
let files = model.findFilesByIds(ids: selections)
48-
files.forEach { file in withAnimation { file.isExpanded.toggle() } }
46+
// one entry.
47+
selections.forEach { entry in withAnimation { entry.isExpanded.toggle() } }
4948
}
5049
).listStyle(.sidebar)
5150
}
@@ -64,20 +63,19 @@ struct FilePicker: View {
6463

6564
private func submit() {
6665
guard let selection else { return }
67-
let files = model.findFilesByIds(ids: [selection])
68-
if let file = files.first {
69-
outputAbsPath = file.absolute_path
70-
}
66+
outputAbsPath = selection.absolute_path
7167
dismiss()
7268
}
7369
}
7470

7571
@MainActor
7672
class FilePickerModel: ObservableObject {
77-
@Published var rootFiles: [FilePickerItemModel] = []
73+
@Published var rootEntries: [FilePickerEntryModel] = []
7874
@Published var rootIsLoading: Bool = false
7975
@Published var error: ClientError?
8076

77+
// It's important that `AgentClient` is a reference type (class)
78+
// as we were having performance issues with a struct (unless it was a binding).
8179
let client: AgentClient
8280

8381
init(host: String) {
@@ -90,82 +88,56 @@ class FilePickerModel: ObservableObject {
9088
Task {
9189
defer { rootIsLoading = false }
9290
do throws(ClientError) {
93-
rootFiles = try await client
91+
rootEntries = try await client
9492
.listAgentDirectory(.init(path: [], relativity: .root))
95-
.toModels(client: client, path: [])
93+
.toModels(client: client)
9694
} catch {
9795
self.error = error
9896
}
9997
}
10098
}
101-
102-
func findFilesByIds(ids: Set<FilePickerItemModel.ID>) -> [FilePickerItemModel] {
103-
var result: [FilePickerItemModel] = []
104-
105-
for id in ids {
106-
if let file = findFileByPath(path: id, in: rootFiles) {
107-
result.append(file)
108-
}
109-
}
110-
111-
return result
112-
}
113-
114-
private func findFileByPath(path: [String], in files: [FilePickerItemModel]?) -> FilePickerItemModel? {
115-
guard let files, !path.isEmpty else { return nil }
116-
117-
if let file = files.first(where: { $0.name == path[0] }) {
118-
if path.count == 1 {
119-
return file
120-
}
121-
// Array slices are just views, so this isn't expensive
122-
return findFileByPath(path: Array(path[1...]), in: file.contents)
123-
}
124-
125-
return nil
126-
}
12799
}
128100

129-
struct FilePickerItem: View {
130-
@ObservedObject var item: FilePickerItemModel
101+
struct FilePickerEntry: View {
102+
@ObservedObject var entry: FilePickerEntryModel
131103

132104
var body: some View {
133105
Group {
134-
if item.dir {
106+
if entry.dir {
135107
directory
136108
} else {
137-
Label(item.name, systemImage: "doc")
138-
.help(item.absolute_path)
109+
Label(entry.name, systemImage: "doc")
110+
.help(entry.absolute_path)
139111
.selectionDisabled()
140112
.foregroundColor(.secondary)
141113
}
142114
}
143115
}
144116

145117
private var directory: some View {
146-
DisclosureGroup(isExpanded: $item.isExpanded) {
147-
if let contents = item.contents {
148-
ForEach(contents) { item in
149-
FilePickerItem(item: item)
118+
DisclosureGroup(isExpanded: $entry.isExpanded) {
119+
if let entries = entry.entries {
120+
ForEach(entries) { entry in
121+
FilePickerEntry(entry: entry).tag(entry)
150122
}
151123
}
152124
} label: {
153125
Label {
154-
Text(item.name)
126+
Text(entry.name)
155127
ZStack {
156-
ProgressView().controlSize(.small).opacity(item.isLoading && item.error == nil ? 1 : 0)
128+
ProgressView().controlSize(.small).opacity(entry.isLoading && entry.error == nil ? 1 : 0)
157129
Image(systemName: "exclamationmark.triangle.fill")
158-
.opacity(item.error != nil ? 1 : 0)
130+
.opacity(entry.error != nil ? 1 : 0)
159131
}
160132
} icon: {
161133
Image(systemName: "folder")
162-
}.help(item.error != nil ? item.error!.description : item.absolute_path)
134+
}.help(entry.error != nil ? entry.error!.description : entry.absolute_path)
163135
}
164136
}
165137
}
166138

167139
@MainActor
168-
class FilePickerItemModel: Identifiable, ObservableObject {
140+
class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
169141
nonisolated let id: [String]
170142
let name: String
171143
// Components of the path as an array
@@ -175,7 +147,7 @@ class FilePickerItemModel: Identifiable, ObservableObject {
175147

176148
let client: AgentClient
177149

178-
@Published var contents: [FilePickerItemModel]?
150+
@Published var entries: [FilePickerEntryModel]?
179151
@Published var isLoading = false
180152
@Published var error: ClientError?
181153
@Published private var innerIsExpanded = false
@@ -186,7 +158,7 @@ class FilePickerItemModel: Identifiable, ObservableObject {
186158
withAnimation { self.innerIsExpanded = false }
187159
} else {
188160
Task {
189-
self.loadContents()
161+
self.loadEntries()
190162
}
191163
}
192164
}
@@ -198,20 +170,20 @@ class FilePickerItemModel: Identifiable, ObservableObject {
198170
absolute_path: String,
199171
path: [String],
200172
dir: Bool = false,
201-
contents: [FilePickerItemModel]? = nil
173+
entries: [FilePickerEntryModel]? = nil
202174
) {
203175
self.name = name
204176
self.client = client
205177
self.path = path
206178
self.dir = dir
207179
self.absolute_path = absolute_path
208-
self.contents = contents
180+
self.entries = entries
209181

210-
// Swift Arrays are COW
182+
// Swift Arrays are copy on write
211183
id = path
212184
}
213185

214-
func loadContents() {
186+
func loadEntries() {
215187
self.error = nil
216188
withAnimation { isLoading = true }
217189
Task {
@@ -222,30 +194,38 @@ class FilePickerItemModel: Identifiable, ObservableObject {
222194
}
223195
}
224196
do throws(ClientError) {
225-
contents = try await client
197+
entries = try await client
226198
.listAgentDirectory(.init(path: path, relativity: .root))
227-
.toModels(client: client, path: path)
199+
.toModels(client: client)
228200
} catch {
229201
self.error = error
230202
}
231203
}
232204
}
205+
206+
nonisolated static func == (lhs: FilePickerEntryModel, rhs: FilePickerEntryModel) -> Bool {
207+
lhs.id == rhs.id
208+
}
209+
210+
nonisolated func hash(into hasher: inout Hasher) {
211+
hasher.combine(id)
212+
}
233213
}
234214

235215
extension LSResponse {
236216
@MainActor
237-
func toModels(client: AgentClient, path: [String]) -> [FilePickerItemModel] {
238-
contents.compactMap { file in
217+
func toModels(client: AgentClient) -> [FilePickerEntryModel] {
218+
contents.compactMap { entry in
239219
// Filter dotfiles from the picker
240-
guard !file.name.hasPrefix(".") else { return nil }
220+
guard !entry.name.hasPrefix(".") else { return nil }
241221

242-
return FilePickerItemModel(
243-
name: file.name,
222+
return FilePickerEntryModel(
223+
name: entry.name,
244224
client: client,
245-
absolute_path: file.absolute_path_string,
246-
path: path + [file.name],
247-
dir: file.is_dir,
248-
contents: nil
225+
absolute_path: entry.absolute_path_string,
226+
path: self.absolute_path + [entry.name],
227+
dir: entry.is_dir,
228+
entries: nil
249229
)
250230
}
251231
}

0 commit comments

Comments
 (0)