@@ -16,12 +16,18 @@ import * as vscode from "vscode";
16
16
import * as fs from "fs/promises" ;
17
17
import * as path from "path" ;
18
18
import configuration from "../configuration" ;
19
- import { getRepositoryName , buildDirectoryFromWorkspacePath } from "../utilities/utilities" ;
19
+ import { buildDirectoryFromWorkspacePath } from "../utilities/utilities" ;
20
20
import { WorkspaceContext } from "../WorkspaceContext" ;
21
21
import { FolderEvent } from "../WorkspaceContext" ;
22
22
import { FolderContext } from "../FolderContext" ;
23
23
import contextKeys from "../contextKeys" ;
24
- import { WorkspaceState } from "../SwiftPackage" ;
24
+ import {
25
+ Dependency ,
26
+ PackageContents ,
27
+ SwiftPackage ,
28
+ WorkspaceState ,
29
+ WorkspaceStateDependency ,
30
+ } from "../SwiftPackage" ;
25
31
26
32
/**
27
33
* References:
@@ -41,6 +47,7 @@ export class PackageNode {
41
47
constructor (
42
48
public name : string ,
43
49
public path : string ,
50
+ public location : string ,
44
51
public version : string ,
45
52
public type : "local" | "remote" | "editing"
46
53
) { }
@@ -148,39 +155,129 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
148
155
}
149
156
if ( ! element ) {
150
157
const workspaceState = await folderContext . swiftPackage . loadWorkspaceState ( ) ;
151
- // Build PackageNodes for all dependencies. Because Package.resolved might not
152
- // be up to date with edited dependency list, we need to remove the edited
153
- // dependencies from the list before adding in the edit version
154
- const children = [
155
- ...this . getLocalDependencies ( workspaceState ) ,
156
- ...this . getRemoteDependencies ( folderContext ) ,
157
- ] ;
158
- const editedChildren = await this . getEditedDependencies ( workspaceState ) ;
159
- const uneditedChildren : PackageNode [ ] = [ ] ;
160
- for ( const child of children ) {
161
- const editedVersion = editedChildren . find ( item => item . name === child . name ) ;
162
- if ( ! editedVersion ) {
163
- uneditedChildren . push ( child ) ;
164
- }
158
+ return await this . getDependencyGraph ( workspaceState , folderContext ) ;
159
+ }
160
+
161
+ return this . getNodesInDirectory ( element . path ) ;
162
+ }
163
+
164
+ private async getDependencyGraph (
165
+ workspaceState : WorkspaceState | undefined ,
166
+ folderContext : FolderContext
167
+ ) : Promise < PackageNode [ ] > {
168
+ if ( ! workspaceState ) {
169
+ return [ ] ;
170
+ }
171
+ const inUseDependencies = await this . getInUseDependencies ( workspaceState , folderContext ) ;
172
+ return (
173
+ workspaceState ?. object . dependencies
174
+ . filter ( dependency => inUseDependencies . has ( dependency . packageRef . identity ) )
175
+ . map ( dependency => {
176
+ const type = this . dependencyType ( dependency ) ;
177
+ const version = this . dependencyDisplayVersion ( dependency ) ;
178
+ const packagePath = this . dependencyPackagePath (
179
+ dependency ,
180
+ folderContext . folder . fsPath
181
+ ) ;
182
+ const location = dependency . packageRef . location ;
183
+ return new PackageNode (
184
+ dependency . packageRef . identity ,
185
+ packagePath ,
186
+ location ,
187
+ version ,
188
+ type
189
+ ) ;
190
+ } ) ?? [ ]
191
+ ) ;
192
+ }
193
+
194
+ /**
195
+ * * Returns a set of all dependencies that are in use in the workspace.
196
+ * Why tranverse is necessary here?
197
+ * * If we have an implicit local dependency of a dependency, you may not be able to see it in either `Package.swift` or `Package.resolved` unless tranversing from root Package.swift.
198
+ * Why not using `swift package show-dependencies`?
199
+ * * it costs more time and it triggers the file change of `workspace-state.json` which is not necessary
200
+ * Why not using `workspace-state.json` directly?
201
+ * * `workspace-state.json` contains all necessary dependencies but it also contains dependencies that are not in use.
202
+ * Here is the implementation details:
203
+ * 1. local/remote/edited dependency has remote/edited dependencies, Package.resolved covers them
204
+ * 2. remote/edited dependency has a local dependency, the local dependency must have been declared in root Package.swift
205
+ * 3. local dependency has a local dependency, traverse it and find the local dependencies only recursively
206
+ * 4. pins include all remote and edited packages for 1, 2
207
+ */
208
+ private async getInUseDependencies (
209
+ workspaceState : WorkspaceState ,
210
+ folderContext : FolderContext
211
+ ) : Promise < Set < string > > {
212
+ const localDependencies = await this . getLocalDependencySet ( workspaceState , folderContext ) ;
213
+ const remoteDependencies = this . getRemoteDependencySet ( folderContext ) ;
214
+ const editedDependencies = this . getEditedDependencySet ( workspaceState ) ;
215
+ return new Set < string > ( [
216
+ ...localDependencies ,
217
+ ...remoteDependencies ,
218
+ ...editedDependencies ,
219
+ ] ) ;
220
+ }
221
+
222
+ private getRemoteDependencySet ( folderContext : FolderContext | undefined ) : Set < string > {
223
+ return new Set < string > ( folderContext ?. swiftPackage . resolved ?. pins . map ( pin => pin . identity ) ) ;
224
+ }
225
+
226
+ private getEditedDependencySet ( workspaceState : WorkspaceState ) : Set < string > {
227
+ return new Set < string > (
228
+ workspaceState . object . dependencies
229
+ . filter ( dependency => this . dependencyType ( dependency ) === "editing" )
230
+ . map ( dependency => dependency . packageRef . identity )
231
+ ) ;
232
+ }
233
+
234
+ /**
235
+ * @param workspaceState the workspace state read from `Workspace-state.json`
236
+ * @param folderContext the folder context of the current folder
237
+ * @returns all local in-use dependencies
238
+ */
239
+ private async getLocalDependencySet (
240
+ workspaceState : WorkspaceState ,
241
+ folderContext : FolderContext
242
+ ) : Promise < Set < string > > {
243
+ const rootDependencies = folderContext . swiftPackage . dependencies ?? [ ] ;
244
+ const workspaceStateDependencies = workspaceState . object . dependencies ?? [ ] ;
245
+ const workspacePath = folderContext . folder . fsPath ;
246
+
247
+ const showingDependencies : Set < string > = new Set < string > ( ) ;
248
+ const stack : Dependency [ ] = rootDependencies ;
249
+
250
+ while ( stack . length > 0 ) {
251
+ const top = stack . pop ( ) ;
252
+ if ( ! top ) {
253
+ continue ;
254
+ }
255
+
256
+ if ( showingDependencies . has ( top . identity ) ) {
257
+ continue ;
258
+ }
259
+
260
+ if ( top . type !== "local" && top . type !== "fileSystem" ) {
261
+ continue ;
165
262
}
166
- return [ ...uneditedChildren , ...editedChildren ] . sort ( ( first , second ) =>
167
- first . name . localeCompare ( second . name )
263
+
264
+ showingDependencies . add ( top . identity ) ;
265
+ const workspaceStateDependency = workspaceStateDependencies . find (
266
+ workspaceStateDependency =>
267
+ workspaceStateDependency . packageRef . identity === top . identity
168
268
) ;
169
- }
269
+ if ( ! workspaceStateDependency ) {
270
+ continue ;
271
+ }
170
272
171
- const buildDirectory = buildDirectoryFromWorkspacePath ( folderContext . folder . fsPath , true ) ;
273
+ const packagePath = this . dependencyPackagePath ( workspaceStateDependency , workspacePath ) ;
274
+ const childDependencyContents = ( await SwiftPackage . loadPackage (
275
+ vscode . Uri . file ( packagePath )
276
+ ) ) as PackageContents ;
172
277
173
- if ( element instanceof PackageNode ) {
174
- // Read the contents of a package.
175
- const packagePath =
176
- element . type === "remote"
177
- ? path . join ( buildDirectory , "checkouts" , getRepositoryName ( element . path ) )
178
- : element . path ;
179
- return this . getNodesInDirectory ( packagePath ) ;
180
- } else {
181
- // Read the contents of a directory within a package.
182
- return this . getNodesInDirectory ( element . path ) ;
278
+ stack . push ( ...childDependencyContents . dependencies ) ;
183
279
}
280
+ return showingDependencies ;
184
281
}
185
282
186
283
/**
@@ -204,6 +301,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
204
301
new PackageNode (
205
302
dependency . packageRef . identity ,
206
303
dependency . packageRef . location ,
304
+ dependency . packageRef . location ,
207
305
"local" ,
208
306
"local"
209
307
)
@@ -221,6 +319,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
221
319
new PackageNode (
222
320
pin . identity ,
223
321
pin . location ,
322
+ pin . location ,
224
323
pin . state . version ?? pin . state . branch ?? pin . state . revision . substring ( 0 , 7 ) ,
225
324
"remote"
226
325
)
@@ -244,6 +343,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
244
343
new PackageNode (
245
344
item . packageRef . identity ,
246
345
item . state . path ! ,
346
+ item . state . path ! ,
247
347
"local" ,
248
348
"editing"
249
349
)
@@ -277,4 +377,74 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
277
377
}
278
378
} ) ;
279
379
}
380
+
381
+ /// - Dependency display helpers
382
+
383
+ /**
384
+ * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local
385
+ * @param dependency
386
+ * @return "local" | "remote" | "editing"
387
+ */
388
+ private dependencyType ( dependency : WorkspaceStateDependency ) : "local" | "remote" | "editing" {
389
+ if ( dependency . state . name === "edited" ) {
390
+ return "editing" ;
391
+ } else if (
392
+ dependency . packageRef . kind === "local" ||
393
+ dependency . packageRef . kind === "fileSystem"
394
+ ) {
395
+ // need to check for both "local" and "fileSystem" as swift 5.5 and earlier
396
+ // use "local" while 5.6 and later use "fileSystem"
397
+ return "local" ;
398
+ } else {
399
+ return "remote" ;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Get version of WorkspaceStateDependency for displaying in the tree
405
+ * @param dependency
406
+ * @return real version | editing | local
407
+ */
408
+ private dependencyDisplayVersion ( dependency : WorkspaceStateDependency ) : string {
409
+ const type = this . dependencyType ( dependency ) ;
410
+ if ( type === "editing" ) {
411
+ return "editing" ;
412
+ } else if ( type === "local" ) {
413
+ return "local" ;
414
+ } else {
415
+ return (
416
+ dependency . state . checkoutState ?. version ??
417
+ dependency . state . checkoutState ?. branch ??
418
+ dependency . state . checkoutState ?. revision . substring ( 0 , 7 ) ??
419
+ "unknown"
420
+ ) ;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * * Get package source path of dependency
426
+ * `editing`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath
427
+ * `local`: dependency.packageRef.location
428
+ * `remote`: buildDirectory + checkouts + dependency.packageRef.location
429
+ * @param dependency
430
+ * @param workspaceFolder
431
+ * @return the package path based on the type
432
+ */
433
+ private dependencyPackagePath (
434
+ dependency : WorkspaceStateDependency ,
435
+ workspaceFolder : string
436
+ ) : string {
437
+ const type = this . dependencyType ( dependency ) ;
438
+ if ( type === "editing" ) {
439
+ return (
440
+ dependency . state . path ?? path . join ( workspaceFolder , "Packages" , dependency . subpath )
441
+ ) ;
442
+ } else if ( type === "local" ) {
443
+ return dependency . state . path ?? dependency . packageRef . location ;
444
+ } else {
445
+ // remote
446
+ const buildDirectory = buildDirectoryFromWorkspacePath ( workspaceFolder , true ) ;
447
+ return path . join ( buildDirectory , "checkouts" , dependency . subpath ) ;
448
+ }
449
+ }
280
450
}
0 commit comments