diff --git a/package.json b/package.json index e523706ec..b047629f0 100644 --- a/package.json +++ b/package.json @@ -310,7 +310,7 @@ }, { "command": "swift.openExternal", - "when": "view == packageDependencies && viewItem == remote" + "when": "view == packageDependencies && viewItem != local" } ] }, diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index 8ad820312..4c9c1c900 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -117,9 +117,19 @@ export interface WorkspaceState { version: number; } +/** revision + (branch || version) + * ref: https://github.com/apple/swift-package-manager/blob/e25a590dc455baa430f2ec97eacc30257c172be2/Sources/Workspace/CheckoutState.swift#L19:L23 + */ +export interface CheckoutState { + revision: string; + branch: string | null; + version: string | null; +} + export interface WorkspaceStateDependency { packageRef: { identity: string; kind: string; location: string; name: string }; - state: { name: string; path?: string }; + state: { name: string; path?: string; checkoutState?: CheckoutState }; + subpath: string; } export interface PackagePlugin { diff --git a/src/commands.ts b/src/commands.ts index cb9425f2c..8d6f230e1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -421,7 +421,7 @@ async function executeTaskWithUI( */ function openInExternalEditor(packageNode: PackageNode) { try { - const uri = vscode.Uri.parse(packageNode.path, true); + const uri = vscode.Uri.parse(packageNode.location, true); vscode.env.openExternal(uri); } catch { // ignore error diff --git a/src/ui/PackageDependencyProvider.ts b/src/ui/PackageDependencyProvider.ts index ab7877197..c108c2acb 100644 --- a/src/ui/PackageDependencyProvider.ts +++ b/src/ui/PackageDependencyProvider.ts @@ -16,12 +16,18 @@ import * as vscode from "vscode"; import * as fs from "fs/promises"; import * as path from "path"; import configuration from "../configuration"; -import { getRepositoryName, buildDirectoryFromWorkspacePath } from "../utilities/utilities"; +import { buildDirectoryFromWorkspacePath } from "../utilities/utilities"; import { WorkspaceContext } from "../WorkspaceContext"; import { FolderEvent } from "../WorkspaceContext"; import { FolderContext } from "../FolderContext"; import contextKeys from "../contextKeys"; -import { WorkspaceState } from "../SwiftPackage"; +import { + Dependency, + PackageContents, + SwiftPackage, + WorkspaceState, + WorkspaceStateDependency, +} from "../SwiftPackage"; /** * References: @@ -41,6 +47,7 @@ export class PackageNode { constructor( public name: string, public path: string, + public location: string, public version: string, public type: "local" | "remote" | "editing" ) {} @@ -148,39 +155,129 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider item.name === child.name); - if (!editedVersion) { - uneditedChildren.push(child); - } + return await this.getDependencyGraph(workspaceState, folderContext); + } + + return this.getNodesInDirectory(element.path); + } + + private async getDependencyGraph( + workspaceState: WorkspaceState | undefined, + folderContext: FolderContext + ): Promise { + if (!workspaceState) { + return []; + } + const inUseDependencies = await this.getInUseDependencies(workspaceState, folderContext); + return ( + workspaceState?.object.dependencies + .filter(dependency => inUseDependencies.has(dependency.packageRef.identity)) + .map(dependency => { + const type = this.dependencyType(dependency); + const version = this.dependencyDisplayVersion(dependency); + const packagePath = this.dependencyPackagePath( + dependency, + folderContext.folder.fsPath + ); + const location = dependency.packageRef.location; + return new PackageNode( + dependency.packageRef.identity, + packagePath, + location, + version, + type + ); + }) ?? [] + ); + } + + /** + * * Returns a set of all dependencies that are in use in the workspace. + * Why tranverse is necessary here? + * * 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. + * Why not using `swift package show-dependencies`? + * * it costs more time and it triggers the file change of `workspace-state.json` which is not necessary + * Why not using `workspace-state.json` directly? + * * `workspace-state.json` contains all necessary dependencies but it also contains dependencies that are not in use. + * Here is the implementation details: + * 1. local/remote/edited dependency has remote/edited dependencies, Package.resolved covers them + * 2. remote/edited dependency has a local dependency, the local dependency must have been declared in root Package.swift + * 3. local dependency has a local dependency, traverse it and find the local dependencies only recursively + * 4. pins include all remote and edited packages for 1, 2 + */ + private async getInUseDependencies( + workspaceState: WorkspaceState, + folderContext: FolderContext + ): Promise> { + const localDependencies = await this.getLocalDependencySet(workspaceState, folderContext); + const remoteDependencies = this.getRemoteDependencySet(folderContext); + const editedDependencies = this.getEditedDependencySet(workspaceState); + return new Set([ + ...localDependencies, + ...remoteDependencies, + ...editedDependencies, + ]); + } + + private getRemoteDependencySet(folderContext: FolderContext | undefined): Set { + return new Set(folderContext?.swiftPackage.resolved?.pins.map(pin => pin.identity)); + } + + private getEditedDependencySet(workspaceState: WorkspaceState): Set { + return new Set( + workspaceState.object.dependencies + .filter(dependency => this.dependencyType(dependency) === "editing") + .map(dependency => dependency.packageRef.identity) + ); + } + + /** + * @param workspaceState the workspace state read from `Workspace-state.json` + * @param folderContext the folder context of the current folder + * @returns all local in-use dependencies + */ + private async getLocalDependencySet( + workspaceState: WorkspaceState, + folderContext: FolderContext + ): Promise> { + const rootDependencies = folderContext.swiftPackage.dependencies ?? []; + const workspaceStateDependencies = workspaceState.object.dependencies ?? []; + const workspacePath = folderContext.folder.fsPath; + + const showingDependencies: Set = new Set(); + const stack: Dependency[] = rootDependencies; + + while (stack.length > 0) { + const top = stack.pop(); + if (!top) { + continue; + } + + if (showingDependencies.has(top.identity)) { + continue; + } + + if (top.type !== "local" && top.type !== "fileSystem") { + continue; } - return [...uneditedChildren, ...editedChildren].sort((first, second) => - first.name.localeCompare(second.name) + + showingDependencies.add(top.identity); + const workspaceStateDependency = workspaceStateDependencies.find( + workspaceStateDependency => + workspaceStateDependency.packageRef.identity === top.identity ); - } + if (!workspaceStateDependency) { + continue; + } - const buildDirectory = buildDirectoryFromWorkspacePath(folderContext.folder.fsPath, true); + const packagePath = this.dependencyPackagePath(workspaceStateDependency, workspacePath); + const childDependencyContents = (await SwiftPackage.loadPackage( + vscode.Uri.file(packagePath) + )) as PackageContents; - if (element instanceof PackageNode) { - // Read the contents of a package. - const packagePath = - element.type === "remote" - ? path.join(buildDirectory, "checkouts", getRepositoryName(element.path)) - : element.path; - return this.getNodesInDirectory(packagePath); - } else { - // Read the contents of a directory within a package. - return this.getNodesInDirectory(element.path); + stack.push(...childDependencyContents.dependencies); } + return showingDependencies; } /** @@ -204,6 +301,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider