diff --git a/package.json b/package.json index e523706ec..50340e2d8 100644 --- a/package.json +++ b/package.json @@ -296,13 +296,17 @@ "command": "swift.useLocalDependency", "when": "view == packageDependencies && viewItem == remote" }, + { + "command": "swift.editDependency", + "when": "view == packageDependencies && viewItem == remote" + }, { "command": "swift.uneditDependency", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == packageDependencies && viewItem == edited" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == packageDependencies && viewItem == edited" }, { "command": "swift.openInWorkspace", @@ -434,3 +438,4 @@ "vscode-languageclient": "^8.0.0" } } + diff --git a/src/FolderContext.ts b/src/FolderContext.ts index f1caa430a..2b6f4a91e 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -16,7 +16,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { LinuxMain } from "./LinuxMain"; import { PackageWatcher } from "./PackageWatcher"; -import { SwiftPackage } from "./SwiftPackage"; +import { SwiftPackage, WorkspaceStateDependency } from "./SwiftPackage"; import { TestExplorer } from "./TestExplorer/TestExplorer"; import { WorkspaceContext, FolderEvent } from "./WorkspaceContext"; import { BackgroundCompilation } from "./BackgroundCompilation"; @@ -150,11 +150,19 @@ export class FolderContext implements vscode.Disposable { return item.state.name === "edited" && item.state.path; }) .map(item => { - return { name: item.packageRef.identity, folder: item.state.path! }; + return { + name: item.packageRef.identity, + folder: item.state.path!, + }; }) ?? [] ); } + /** Get list in-use packages */ + async resolveDependencyGraph(): Promise { + return await this.swiftPackage.resolveDependencyGraph(); + } + static uriName(uri: vscode.Uri): string { return path.basename(uri.fsPath); } diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index f7cea0cc8..d7c6f799c 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import { FolderContext } from "./FolderContext"; +import { buildDirectoryFromWorkspacePath } from "./utilities/utilities"; import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; /** @@ -25,6 +26,7 @@ import { FolderEvent, WorkspaceContext } from "./WorkspaceContext"; export class PackageWatcher { private packageFileWatcher?: vscode.FileSystemWatcher; private resolvedFileWatcher?: vscode.FileSystemWatcher; + private workspaceStateFileWatcher?: vscode.FileSystemWatcher; constructor(private folderContext: FolderContext, private workspaceContext: WorkspaceContext) {} @@ -35,6 +37,7 @@ export class PackageWatcher { install() { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); + this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher(); } /** @@ -44,6 +47,7 @@ export class PackageWatcher { dispose() { this.packageFileWatcher?.dispose(); this.resolvedFileWatcher?.dispose(); + this.workspaceStateFileWatcher?.dispose(); } private createPackageFileWatcher(): vscode.FileSystemWatcher { @@ -66,6 +70,20 @@ export class PackageWatcher { return watcher; } + private createWorkspaceStateFileWatcher(): vscode.FileSystemWatcher { + const uri = vscode.Uri.file( + buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true) + ); + + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(uri, "workspace-state.json") + ); + watcher.onDidCreate(async () => await this.handleWorkspaceStateChange()); + watcher.onDidChange(async () => await this.handleWorkspaceStateChange()); + watcher.onDidDelete(async () => await this.handleWorkspaceStateChange()); + return watcher; + } + /** * Handles a create or change event for **Package.swift**. * @@ -88,4 +106,13 @@ export class PackageWatcher { await this.folderContext.reloadPackageResolved(); this.workspaceContext.fireEvent(this.folderContext, FolderEvent.resolvedUpdated); } + + /** + * Handles a create or change event for **workspace-state.json**. + * + * This will resolve any changes in the workspace-state.json + */ + private async handleWorkspaceStateChange() { + this.workspaceContext.fireEvent(this.folderContext, FolderEvent.workspaceStateUpdated); + } } diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index 8ad820312..777f2b932 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import * as fs from "fs/promises"; +import * as path from "path"; import { buildDirectoryFromWorkspacePath, execSwift, @@ -117,9 +118,17 @@ export interface WorkspaceState { version: number; } +/** branch + revision || revision + version */ +export interface CheckoutState { + branch: string | null; + revision: string; + 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 { @@ -143,6 +152,61 @@ function isError(state: SwiftPackageState): state is Error { return state instanceof Error; } +/** + * Get version of WorkspaceStateDependency for displaying in the tree + * @param dependency + * @return real version | edited | local + */ +export function dependencyVersion(dependency: WorkspaceStateDependency): string { + return dependency.packageRef.kind === "fileSystem" + ? "local" + : dependency.state.checkoutState?.version ?? + dependency.state.checkoutState?.branch ?? + "edited"; +} + +/** + * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local + * @param dependency + * @return "local" | "remote" | "edited" + */ +export function dependencyType( + dependency: WorkspaceStateDependency +): "local" | "remote" | "edited" { + return dependency.state.name === "edited" + ? "edited" + : dependency.packageRef.kind === "fileSystem" + ? "local" + : "remote"; +} + +/** + * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local + * `edited`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath + * `local`: dependency.packageRef.location + * `remote`: buildDirectory + checkouts + dependency.packageRef.location + * @param dependency + * @return the package path based on the type + */ +export function dependencyPackagePath( + dependency: WorkspaceStateDependency, + workspaceFolder: string +): string { + const type = dependencyType(dependency); + let packagePath = ""; + if (type === "edited") { + packagePath = + dependency.state.path ?? path.join(workspaceFolder, "Packages", dependency.subpath); + } else if (type === "local") { + packagePath = dependency.state.path ?? dependency.packageRef.location; + } else { + // remote + const buildDirectory = buildDirectoryFromWorkspacePath(workspaceFolder, true); + packagePath = path.join(buildDirectory, "checkouts", dependency.subpath); + } + return packagePath; +} + /** * Class holding Swift Package Manager Package */ @@ -267,6 +331,73 @@ export class SwiftPackage implements PackageContents { } } + /** + * tranverse the dependency tree + * in each node, call `swift package describe` to get the child dependencies and do it recursively + * @returns all dependencies in flat list + */ + async resolveDependencyGraph(): Promise { + const workspaceState = await this.loadWorkspaceState(); + const workspaceStateDependencies = workspaceState?.object.dependencies ?? []; + + if (workspaceStateDependencies.length === 0) { + return []; + } + + const contents = this.contents as PackageContents; + const showingDependencies = new Set(); + await this.getChildDependencies(contents, workspaceStateDependencies, showingDependencies); + + // filter workspaceStateDependencies that in showingDependencies + return workspaceStateDependencies.filter(dependency => + showingDependencies.has(dependency.packageRef.identity) + ); + } + + /** + * tranverse the dependency tree + * @param dependency current node + * @param workspaceStateDependencies all dependencies in workspace-state.json + * @param showingDependencies result of dependencies that are showing in the tree + * @returns + */ + private async getChildDependencies( + dependency: PackageContents, + workspaceStateDependencies: WorkspaceStateDependency[], + showingDependencies: Set + ) { + for (let i = 0; i < dependency.dependencies.length; i++) { + const childDependency = dependency.dependencies[i]; + if (showingDependencies.has(childDependency.identity)) { + return; + } + showingDependencies.add(childDependency.identity); + const workspaceStateDependency = workspaceStateDependencies.find( + workspaceStateDependency => + workspaceStateDependency.packageRef.identity === childDependency.identity + ); + if (workspaceStateDependency) { + showingDependencies.add(workspaceStateDependency.packageRef.identity); + } + + if (workspaceStateDependency === undefined) { + return; + } + + const packagePath = dependencyPackagePath(workspaceStateDependency, this.folder.fsPath); + + const childDependencyContents = (await SwiftPackage.loadPackage( + vscode.Uri.file(packagePath) + )) as PackageContents; + + await this.getChildDependencies( + childDependencyContents, + workspaceStateDependencies, + showingDependencies + ); + } + } + /** Reload swift package */ public async reload() { this.contents = await SwiftPackage.loadPackage(this.folder); diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 11d67f9a9..eac7473e9 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -131,9 +131,12 @@ export class WorkspaceContext implements vscode.Disposable { this.updateContextKeys(folder); break; case FolderEvent.resolvedUpdated: + break; + case FolderEvent.workspaceStateUpdated: if (folder === this.currentFolder) { this.updateContextKeys(folder); } + break; } }); this.subscriptions = [ @@ -491,6 +494,8 @@ export enum FolderEvent { packageUpdated = "packageUpdated", // Package.resolved has been updated resolvedUpdated = "resolvedUpdated", + // `workspace-state.json` is updated, update dependency tree only by this event + workspaceStateUpdated = "workspaceStateUpdated", } /** Workspace Folder observer function */ diff --git a/src/commands.ts b/src/commands.ts index cb9425f2c..aea2a80e1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -432,11 +432,12 @@ function updateAfterError(result: boolean, folderContext: FolderContext) { const triggerResolvedUpdatedEvent = folderContext.hasResolveErrors; // set has resolve errors flag folderContext.hasResolveErrors = !result; + // if previous folder state was with resolve errors, and now it is without then - // send Package.resolved updated event to trigger display of package dependencies + // send workspace-state.json updated event to trigger display of package dependencies // view if (triggerResolvedUpdatedEvent && !folderContext.hasResolveErrors) { - folderContext.fireEvent(FolderEvent.resolvedUpdated); + folderContext.fireEvent(FolderEvent.workspaceStateUpdated); } } @@ -455,17 +456,17 @@ export function register(ctx: WorkspaceContext) { vscode.commands.registerCommand("swift.openPackage", () => openPackage(ctx)), vscode.commands.registerCommand("swift.useLocalDependency", item => { if (item instanceof PackageNode) { - useLocalDependency(item.name, ctx); + useLocalDependency(item.identity, ctx); } }), vscode.commands.registerCommand("swift.editDependency", item => { if (item instanceof PackageNode) { - editDependency(item.name, ctx); + editDependency(item.identity, ctx); } }), vscode.commands.registerCommand("swift.uneditDependency", item => { if (item instanceof PackageNode) { - uneditDependency(item.name, ctx); + uneditDependency(item.identity, ctx); } }), vscode.commands.registerCommand("swift.openInWorkspace", item => { diff --git a/src/ui/PackageDependencyProvider.ts b/src/ui/PackageDependencyProvider.ts index cc2c9ad92..7ee18a51e 100644 --- a/src/ui/PackageDependencyProvider.ts +++ b/src/ui/PackageDependencyProvider.ts @@ -16,12 +16,12 @@ 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 { WorkspaceContext } from "../WorkspaceContext"; import { FolderEvent } from "../WorkspaceContext"; import { FolderContext } from "../FolderContext"; import contextKeys from "../contextKeys"; import { Version } from "../utilities/version"; +import { dependencyVersion, dependencyType, dependencyPackagePath } from "../SwiftPackage"; /** * References: @@ -39,10 +39,11 @@ import { Version } from "../utilities/version"; */ export class PackageNode { constructor( + public identity: string, public name: string, public path: string, public version: string, - public type: "local" | "remote" | "editing" + public type: "local" | "remote" | "edited" ) {} toTreeItem(): vscode.TreeItem { @@ -50,9 +51,7 @@ export class PackageNode { item.id = this.path; item.description = this.version; item.iconPath = - this.type === "editing" - ? new vscode.ThemeIcon("edit") - : new vscode.ThemeIcon("package"); + this.type === "edited" ? new vscode.ThemeIcon("edit") : new vscode.ThemeIcon("package"); item.contextValue = this.type; return item; } @@ -126,11 +125,13 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider item.name === child.name); - if (!editedVersion) { - uneditedChildren.push(child); - } - } - return [...uneditedChildren, ...editedChildren].sort((first, second) => - first.name.localeCompare(second.name) - ); + return await this.getDependencyGraph(folderContext); } - const buildDirectory = buildDirectoryFromWorkspacePath(folderContext.folder.fsPath, true); + // Read the contents of a package. + return this.getNodesInDirectory(element.path); + } - 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); - } + private async getDependencyGraph(folderContext: FolderContext): Promise { + const graph = await folderContext.resolveDependencyGraph(); + return graph.map(dependency => { + const version = dependencyVersion(dependency); + const type = dependencyType(dependency); + const packagePath = dependencyPackagePath(dependency, folderContext.folder.fsPath); + + return new PackageNode( + dependency.packageRef.identity, + dependency.packageRef.name, + packagePath, + version, + type + ); + }); } /** @@ -194,7 +187,13 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider !dependency.requirement && dependency.url) .map( dependency => - new PackageNode(dependency.identity, dependency.url!, "local", "local") + new PackageNode( + dependency.identity, + dependency.identity, + dependency.url!, + "local", + "local" + ) ); } else { // since Swift 5.6 local dependencies have `type` `fileSystem` @@ -202,7 +201,13 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider dependency.type === "fileSystem" && dependency.path) .map( dependency => - new PackageNode(dependency.identity, dependency.path!, "local", "local") + new PackageNode( + dependency.identity, + dependency.identity, + dependency.path!, + "local", + "local" + ) ); } } @@ -215,6 +220,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider new PackageNode( + pin.identity, pin.identity, pin.location, pin.state.version ?? pin.state.branch ?? pin.state.revision.substring(0, 7), @@ -231,7 +237,7 @@ export class PackageDependenciesProvider implements vscode.TreeDataProvider { return (await folderContext.getEditedPackages()).map( - item => new PackageNode(item.name, item.folder, "local", "editing") + item => new PackageNode(item.name, item.name, item.folder, "local", "edited") ); }