@@ -5,7 +5,7 @@ import SwiftUI
5
5
struct FilePicker : View {
6
6
@Environment ( \. dismiss) var dismiss
7
7
@StateObject private var model : FilePickerModel
8
- @State private var selection : FilePickerItemModel . ID ?
8
+ @State private var selection : FilePickerEntryModel ?
9
9
10
10
@Binding var outputAbsPath : String
11
11
@@ -35,17 +35,16 @@ struct FilePicker: View {
35
35
. padding ( )
36
36
} else {
37
37
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 )
40
40
}
41
41
} . contextMenu (
42
- forSelectionType: FilePickerItemModel . ID . self,
42
+ forSelectionType: FilePickerEntryModel . self,
43
43
menu: { _ in } ,
44
44
primaryAction: { selections in
45
45
// 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 ( ) } }
49
48
}
50
49
) . listStyle ( . sidebar)
51
50
}
@@ -64,20 +63,19 @@ struct FilePicker: View {
64
63
65
64
private func submit( ) {
66
65
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
71
67
dismiss ( )
72
68
}
73
69
}
74
70
75
71
@MainActor
76
72
class FilePickerModel : ObservableObject {
77
- @Published var rootFiles : [ FilePickerItemModel ] = [ ]
73
+ @Published var rootEntries : [ FilePickerEntryModel ] = [ ]
78
74
@Published var rootIsLoading : Bool = false
79
75
@Published var error : ClientError ?
80
76
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).
81
79
let client : AgentClient
82
80
83
81
init ( host: String ) {
@@ -90,82 +88,56 @@ class FilePickerModel: ObservableObject {
90
88
Task {
91
89
defer { rootIsLoading = false }
92
90
do throws ( ClientError) {
93
- rootFiles = try await client
91
+ rootEntries = try await client
94
92
. listAgentDirectory ( . init( path: [ ] , relativity: . root) )
95
- . toModels ( client: client, path : [ ] )
93
+ . toModels ( client: client)
96
94
} catch {
97
95
self . error = error
98
96
}
99
97
}
100
98
}
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
- }
127
99
}
128
100
129
- struct FilePickerItem : View {
130
- @ObservedObject var item : FilePickerItemModel
101
+ struct FilePickerEntry : View {
102
+ @ObservedObject var entry : FilePickerEntryModel
131
103
132
104
var body : some View {
133
105
Group {
134
- if item . dir {
106
+ if entry . dir {
135
107
directory
136
108
} else {
137
- Label ( item . name, systemImage: " doc " )
138
- . help ( item . absolute_path)
109
+ Label ( entry . name, systemImage: " doc " )
110
+ . help ( entry . absolute_path)
139
111
. selectionDisabled ( )
140
112
. foregroundColor ( . secondary)
141
113
}
142
114
}
143
115
}
144
116
145
117
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 )
150
122
}
151
123
}
152
124
} label: {
153
125
Label {
154
- Text ( item . name)
126
+ Text ( entry . name)
155
127
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 )
157
129
Image ( systemName: " exclamationmark.triangle.fill " )
158
- . opacity ( item . error != nil ? 1 : 0 )
130
+ . opacity ( entry . error != nil ? 1 : 0 )
159
131
}
160
132
} icon: {
161
133
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)
163
135
}
164
136
}
165
137
}
166
138
167
139
@MainActor
168
- class FilePickerItemModel : Identifiable , ObservableObject {
140
+ class FilePickerEntryModel : Identifiable , Hashable , ObservableObject {
169
141
nonisolated let id : [ String ]
170
142
let name : String
171
143
// Components of the path as an array
@@ -175,7 +147,7 @@ class FilePickerItemModel: Identifiable, ObservableObject {
175
147
176
148
let client : AgentClient
177
149
178
- @Published var contents : [ FilePickerItemModel ] ?
150
+ @Published var entries : [ FilePickerEntryModel ] ?
179
151
@Published var isLoading = false
180
152
@Published var error : ClientError ?
181
153
@Published private var innerIsExpanded = false
@@ -186,7 +158,7 @@ class FilePickerItemModel: Identifiable, ObservableObject {
186
158
withAnimation { self . innerIsExpanded = false }
187
159
} else {
188
160
Task {
189
- self . loadContents ( )
161
+ self . loadEntries ( )
190
162
}
191
163
}
192
164
}
@@ -198,20 +170,20 @@ class FilePickerItemModel: Identifiable, ObservableObject {
198
170
absolute_path: String ,
199
171
path: [ String ] ,
200
172
dir: Bool = false ,
201
- contents : [ FilePickerItemModel ] ? = nil
173
+ entries : [ FilePickerEntryModel ] ? = nil
202
174
) {
203
175
self . name = name
204
176
self . client = client
205
177
self . path = path
206
178
self . dir = dir
207
179
self . absolute_path = absolute_path
208
- self . contents = contents
180
+ self . entries = entries
209
181
210
- // Swift Arrays are COW
182
+ // Swift Arrays are copy on write
211
183
id = path
212
184
}
213
185
214
- func loadContents ( ) {
186
+ func loadEntries ( ) {
215
187
self . error = nil
216
188
withAnimation { isLoading = true }
217
189
Task {
@@ -222,30 +194,38 @@ class FilePickerItemModel: Identifiable, ObservableObject {
222
194
}
223
195
}
224
196
do throws ( ClientError) {
225
- contents = try await client
197
+ entries = try await client
226
198
. listAgentDirectory ( . init( path: path, relativity: . root) )
227
- . toModels ( client: client, path : path )
199
+ . toModels ( client: client)
228
200
} catch {
229
201
self . error = error
230
202
}
231
203
}
232
204
}
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
+ }
233
213
}
234
214
235
215
extension LSResponse {
236
216
@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
239
219
// Filter dotfiles from the picker
240
- guard !file . name. hasPrefix ( " . " ) else { return nil }
220
+ guard !entry . name. hasPrefix ( " . " ) else { return nil }
241
221
242
- return FilePickerItemModel (
243
- name: file . name,
222
+ return FilePickerEntryModel (
223
+ name: entry . name,
244
224
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
249
229
)
250
230
}
251
231
}
0 commit comments