Skip to content

Commit 1870afe

Browse files
authored
traverse local depedencies from package.swift (#382)
* fixed read dependencies from workspace-state.json * clean unused import * add location for PackageNode and fix `View repository` * fixed editing package can `view repository` * traverse local dependencies * added comments for dependencies functions, simplified code * change recursive to stack, add getEdited
1 parent 9ba47c6 commit 1870afe

File tree

4 files changed

+213
-33
lines changed

4 files changed

+213
-33
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@
323323
},
324324
{
325325
"command": "swift.openExternal",
326-
"when": "view == packageDependencies && viewItem == remote"
326+
"when": "view == packageDependencies && viewItem != local"
327327
}
328328
]
329329
},

src/SwiftPackage.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,19 @@ export interface WorkspaceState {
117117
version: number;
118118
}
119119

120+
/** revision + (branch || version)
121+
* ref: https://github.com/apple/swift-package-manager/blob/e25a590dc455baa430f2ec97eacc30257c172be2/Sources/Workspace/CheckoutState.swift#L19:L23
122+
*/
123+
export interface CheckoutState {
124+
revision: string;
125+
branch: string | null;
126+
version: string | null;
127+
}
128+
120129
export interface WorkspaceStateDependency {
121130
packageRef: { identity: string; kind: string; location: string; name: string };
122-
state: { name: string; path?: string };
131+
state: { name: string; path?: string; checkoutState?: CheckoutState };
132+
subpath: string;
123133
}
124134

125135
export interface PackagePlugin {

src/commands.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ async function executeTaskWithUI(
424424
*/
425425
function openInExternalEditor(packageNode: PackageNode) {
426426
try {
427-
const uri = vscode.Uri.parse(packageNode.path, true);
427+
const uri = vscode.Uri.parse(packageNode.location, true);
428428
vscode.env.openExternal(uri);
429429
} catch {
430430
// ignore error

src/ui/PackageDependencyProvider.ts

+200-30
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ import * as vscode from "vscode";
1616
import * as fs from "fs/promises";
1717
import * as path from "path";
1818
import configuration from "../configuration";
19-
import { getRepositoryName, buildDirectoryFromWorkspacePath } from "../utilities/utilities";
19+
import { buildDirectoryFromWorkspacePath } from "../utilities/utilities";
2020
import { WorkspaceContext } from "../WorkspaceContext";
2121
import { FolderEvent } from "../WorkspaceContext";
2222
import { FolderContext } from "../FolderContext";
2323
import contextKeys from "../contextKeys";
24-
import { WorkspaceState } from "../SwiftPackage";
24+
import {
25+
Dependency,
26+
PackageContents,
27+
SwiftPackage,
28+
WorkspaceState,
29+
WorkspaceStateDependency,
30+
} from "../SwiftPackage";
2531

2632
/**
2733
* References:
@@ -41,6 +47,7 @@ export class PackageNode {
4147
constructor(
4248
public name: string,
4349
public path: string,
50+
public location: string,
4451
public version: string,
4552
public type: "local" | "remote" | "editing"
4653
) {}
@@ -148,39 +155,129 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
148155
}
149156
if (!element) {
150157
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;
165262
}
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
168268
);
169-
}
269+
if (!workspaceStateDependency) {
270+
continue;
271+
}
170272

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;
172277

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);
183279
}
280+
return showingDependencies;
184281
}
185282

186283
/**
@@ -204,6 +301,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
204301
new PackageNode(
205302
dependency.packageRef.identity,
206303
dependency.packageRef.location,
304+
dependency.packageRef.location,
207305
"local",
208306
"local"
209307
)
@@ -221,6 +319,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
221319
new PackageNode(
222320
pin.identity,
223321
pin.location,
322+
pin.location,
224323
pin.state.version ?? pin.state.branch ?? pin.state.revision.substring(0, 7),
225324
"remote"
226325
)
@@ -244,6 +343,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
244343
new PackageNode(
245344
item.packageRef.identity,
246345
item.state.path!,
346+
item.state.path!,
247347
"local",
248348
"editing"
249349
)
@@ -277,4 +377,74 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider<Tree
277377
}
278378
});
279379
}
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+
}
280450
}

0 commit comments

Comments
 (0)