Skip to content

Commit d37b377

Browse files
sort Swiftly toolchains in descending order (#1870)
1 parent 179c0ae commit d37b377

File tree

6 files changed

+416
-373
lines changed

6 files changed

+416
-373
lines changed

src/commands/installSwiftlyToolchain.ts

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,11 @@ import { QuickPickItem } from "vscode";
1616

1717
import { WorkspaceContext } from "../WorkspaceContext";
1818
import { SwiftLogger } from "../logging/SwiftLogger";
19-
import {
20-
Swiftly,
21-
SwiftlyProgressData,
22-
SwiftlyToolchain,
23-
isSnapshotVersion,
24-
isStableVersion,
25-
} from "../toolchain/swiftly";
19+
import { AvailableToolchain, Swiftly, SwiftlyProgressData } from "../toolchain/swiftly";
2620
import { showReloadExtensionNotification } from "../ui/ReloadExtension";
2721

2822
interface SwiftlyToolchainItem extends QuickPickItem {
29-
toolchain: SwiftlyToolchain;
23+
toolchain: AvailableToolchain;
3024
}
3125

3226
async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx: WorkspaceContext) {
@@ -147,15 +141,10 @@ export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise<vo
147141
return;
148142
}
149143

150-
// Sort toolchains with most recent versions first and filter only stable releases
151-
const sortedToolchains = sortToolchainsByVersion(
152-
uninstalledToolchains.filter(toolchain => toolchain.version.type === "stable")
153-
);
154-
155144
ctx.logger.debug(
156-
`Available toolchains for installation: ${sortedToolchains.map(t => t.version.name).join(", ")}`
145+
`Available toolchains for installation: ${uninstalledToolchains.map(t => t.version.name).join(", ")}`
157146
);
158-
const quickPickItems = sortedToolchains.map(toolchain => ({
147+
const quickPickItems = uninstalledToolchains.map(toolchain => ({
159148
label: `$(cloud-download) ${toolchain.version.name}`,
160149
toolchain: toolchain,
161150
}));
@@ -226,10 +215,7 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr
226215
return;
227216
}
228217

229-
// Sort toolchains with most recent versions first
230-
const sortedToolchains = sortToolchainsByVersion(uninstalledSnapshotToolchains);
231-
232-
const quickPickItems = sortedToolchains.map(toolchain => ({
218+
const quickPickItems = uninstalledSnapshotToolchains.map(toolchain => ({
233219
label: `$(cloud-download) ${toolchain.version.name}`,
234220
description: "snapshot",
235221
detail: `Date: ${
@@ -250,44 +236,3 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr
250236

251237
await downloadAndInstallToolchain(selected, ctx);
252238
}
253-
254-
/**
255-
* Sorts toolchains by version with most recent first
256-
*/
257-
function sortToolchainsByVersion(toolchains: SwiftlyToolchain[]): SwiftlyToolchain[] {
258-
return toolchains.sort((a, b) => {
259-
// First sort by type (stable before snapshot)
260-
if (a.version.type !== b.version.type) {
261-
return isStableVersion(a.version) ? -1 : 1;
262-
}
263-
264-
// For stable releases, sort by semantic version
265-
if (isStableVersion(a.version) && isStableVersion(b.version)) {
266-
const versionA = a.version;
267-
const versionB = b.version;
268-
269-
if (versionA && versionB) {
270-
if (versionA.major !== versionB.major) {
271-
return versionB.major - versionA.major;
272-
}
273-
if (versionA.minor !== versionB.minor) {
274-
return versionB.minor - versionA.minor;
275-
}
276-
return versionB.patch - versionA.patch;
277-
}
278-
}
279-
280-
// For snapshots, sort by date (newer first)
281-
if (isSnapshotVersion(a.version) && isSnapshotVersion(b.version)) {
282-
const dateA = a.version.date;
283-
const dateB = b.version.date;
284-
285-
if (dateA && dateB) {
286-
return dateB.localeCompare(dateA);
287-
}
288-
}
289-
290-
// Fallback to string comparison
291-
return b.version.name.localeCompare(a.version.name);
292-
});
293-
}

src/toolchain/swiftly.ts

Lines changed: 106 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -31,91 +31,77 @@ import { ExecFileError, execFile, execFileStreamOutput } from "../utilities/util
3131
import { Version } from "../utilities/version";
3232
import { SwiftlyConfig } from "./ToolchainVersion";
3333

34-
const ListResult = z.object({
35-
toolchains: z.array(
36-
z.object({
37-
inUse: z.boolean(),
38-
isDefault: z.boolean(),
39-
version: z.union([
40-
z.object({
41-
major: z.union([z.number(), z.undefined()]),
42-
minor: z.union([z.number(), z.undefined()]),
43-
patch: z.union([z.number(), z.undefined()]),
44-
name: z.string(),
45-
type: z.literal("stable"),
46-
}),
47-
z.object({
48-
major: z.union([z.number(), z.undefined()]),
49-
minor: z.union([z.number(), z.undefined()]),
50-
branch: z.string(),
51-
date: z.string(),
52-
name: z.string(),
53-
type: z.literal("snapshot"),
54-
}),
55-
z.object({
56-
name: z.string(),
57-
type: z.literal("system"),
58-
}),
59-
z.object(),
60-
]),
61-
})
62-
),
63-
});
64-
65-
const InUseVersionResult = z.object({
66-
version: z.string(),
34+
const SystemVersion = z.object({
35+
type: z.literal("system"),
36+
name: z.string(),
6737
});
38+
export type SystemVersion = z.infer<typeof SystemVersion>;
6839

6940
const StableVersion = z.object({
41+
type: z.literal("stable"),
42+
name: z.string(),
43+
7044
major: z.number(),
7145
minor: z.number(),
7246
patch: z.number(),
73-
name: z.string(),
74-
type: z.literal("stable"),
7547
});
76-
7748
export type StableVersion = z.infer<typeof StableVersion>;
7849

7950
const SnapshotVersion = z.object({
80-
major: z.union([z.number(), z.undefined()]),
81-
minor: z.union([z.number(), z.undefined()]),
51+
type: z.literal("snapshot"),
52+
name: z.string(),
53+
54+
major: z.optional(z.number()),
55+
minor: z.optional(z.number()),
8256
branch: z.string(),
8357
date: z.string(),
84-
name: z.string(),
85-
type: z.literal("snapshot"),
8658
});
87-
8859
export type SnapshotVersion = z.infer<typeof SnapshotVersion>;
8960

90-
export interface SwiftlyToolchain {
61+
export type ToolchainVersion = SystemVersion | StableVersion | SnapshotVersion;
62+
63+
export interface AvailableToolchain {
9164
inUse: boolean;
9265
installed: boolean;
9366
isDefault: boolean;
94-
version: StableVersion | SnapshotVersion;
67+
version: ToolchainVersion;
9568
}
9669

97-
const AvailableToolchain = z.object({
98-
inUse: z.boolean(),
99-
installed: z.boolean(),
100-
isDefault: z.boolean(),
101-
version: z.union([StableVersion, SnapshotVersion, z.object()]),
70+
const SwiftlyListResult = z.object({
71+
toolchains: z.array(
72+
z.object({
73+
inUse: z.boolean(),
74+
isDefault: z.boolean(),
75+
version: z.union([
76+
SystemVersion,
77+
StableVersion,
78+
SnapshotVersion,
79+
// Allow matching against unexpected future version types
80+
z.object(),
81+
]),
82+
})
83+
),
10284
});
103-
type AvailableToolchain = z.infer<typeof AvailableToolchain>;
104-
105-
export function isStableVersion(
106-
version: StableVersion | SnapshotVersion
107-
): version is StableVersion {
108-
return version.type === "stable";
109-
}
11085

111-
export function isSnapshotVersion(
112-
version: StableVersion | SnapshotVersion
113-
): version is SnapshotVersion {
114-
return version.type === "snapshot";
115-
}
86+
const SwiftlyListAvailableResult = z.object({
87+
toolchains: z.array(
88+
z.object({
89+
inUse: z.boolean(),
90+
installed: z.boolean(),
91+
isDefault: z.boolean(),
92+
version: z.union([
93+
SystemVersion,
94+
StableVersion,
95+
SnapshotVersion,
96+
// Allow matching against unexpected future version types
97+
z.object(),
98+
]),
99+
})
100+
),
101+
});
116102

117-
const ListAvailableResult = z.object({
118-
toolchains: z.array(AvailableToolchain),
103+
const InUseVersionResult = z.object({
104+
version: z.string(),
119105
});
120106

121107
export interface SwiftlyProgressData {
@@ -228,6 +214,8 @@ export class Swiftly {
228214
/**
229215
* Finds the list of toolchains installed via Swiftly.
230216
*
217+
* Toolchains will be sorted by version number in descending order.
218+
*
231219
* @returns an array of toolchain version names.
232220
*/
233221
public static async list(logger?: SwiftLogger): Promise<string[]> {
@@ -250,10 +238,13 @@ export class Swiftly {
250238
private static async listUsingJSONFormat(logger?: SwiftLogger): Promise<string[]> {
251239
try {
252240
const { stdout } = await execFile("swiftly", ["list", "--format=json"]);
253-
const response = ListResult.parse(JSON.parse(stdout));
254-
return response.toolchains
255-
.filter(t => ["stable", "snapshot", "system"].includes(t.version?.type))
256-
.map(t => t.version.name);
241+
return SwiftlyListResult.parse(JSON.parse(stdout))
242+
.toolchains.map(toolchain => toolchain.version)
243+
.filter((version): version is ToolchainVersion =>
244+
["system", "stable", "snapshot"].includes(version.type)
245+
)
246+
.sort(compareSwiftlyToolchainVersion)
247+
.map(version => version.name);
257248
} catch (error) {
258249
logger?.error(`Failed to retrieve Swiftly installations: ${error}`);
259250
return [];
@@ -274,8 +265,14 @@ export class Swiftly {
274265
if (!Array.isArray(installedToolchains)) {
275266
return [];
276267
}
277-
return installedToolchains.filter(
278-
(toolchain): toolchain is string => typeof toolchain === "string"
268+
return (
269+
installedToolchains
270+
.filter((toolchain): toolchain is string => typeof toolchain === "string")
271+
// Sort alphabetically in descending order.
272+
//
273+
// This isn't perfect (e.g. "5.10" will come before "5.9"), but this is
274+
// good enough for legacy support.
275+
.sort((lhs, rhs) => rhs.localeCompare(lhs))
279276
);
280277
} catch (error) {
281278
logger?.error(`Failed to retrieve Swiftly installations: ${error}`);
@@ -421,14 +418,16 @@ export class Swiftly {
421418
/**
422419
* Lists all toolchains available for installation from swiftly.
423420
*
421+
* Toolchains will be sorted by version number in descending order.
422+
*
424423
* @param branch Optional branch to filter available toolchains (e.g., "main" for snapshots).
425424
* @param logger Optional logger for error reporting.
426425
* @returns Array of available toolchains.
427426
*/
428427
public static async listAvailable(
429428
branch?: string,
430429
logger?: SwiftLogger
431-
): Promise<SwiftlyToolchain[]> {
430+
): Promise<AvailableToolchain[]> {
432431
if (!this.isSupported()) {
433432
return [];
434433
}
@@ -450,10 +449,11 @@ export class Swiftly {
450449
args.push(branch);
451450
}
452451
const { stdout: availableStdout } = await execFile("swiftly", args);
453-
const result = ListAvailableResult.parse(JSON.parse(availableStdout));
454-
return result.toolchains.filter((t): t is SwiftlyToolchain =>
455-
["stable", "snapshot"].includes(t.version.type)
456-
);
452+
return SwiftlyListAvailableResult.parse(JSON.parse(availableStdout))
453+
.toolchains.filter((t): t is AvailableToolchain =>
454+
["system", "stable", "snapshot"].includes(t.version.type)
455+
)
456+
.sort(compareSwiftlyToolchain);
457457
} catch (error) {
458458
logger?.error(`Failed to retrieve available Swiftly toolchains: ${error}`);
459459
return [];
@@ -878,3 +878,36 @@ export function checkForSwiftlyInstallation(contextKeys: ContextKeys, logger: Sw
878878
});
879879
});
880880
}
881+
882+
function compareSwiftlyToolchain(lhs: AvailableToolchain, rhs: AvailableToolchain): number {
883+
return compareSwiftlyToolchainVersion(lhs.version, rhs.version);
884+
}
885+
886+
function compareSwiftlyToolchainVersion(lhs: ToolchainVersion, rhs: ToolchainVersion): number {
887+
switch (lhs.type) {
888+
case "system": {
889+
if (rhs.type === "system") {
890+
return lhs.name.localeCompare(rhs.name);
891+
}
892+
return -1;
893+
}
894+
case "stable": {
895+
if (rhs.type === "stable") {
896+
const lhsVersion = new Version(lhs.major, lhs.minor, lhs.patch);
897+
const rhsVersion = new Version(rhs.major, rhs.minor, rhs.patch);
898+
return rhsVersion.compare(lhsVersion);
899+
}
900+
if (rhs.type === "system") {
901+
return 1;
902+
}
903+
return -1;
904+
}
905+
case "snapshot":
906+
if (rhs.type === "snapshot") {
907+
const lhsDate = new Date(lhs.date);
908+
const rhsDate = new Date(rhs.date);
909+
return rhsDate.getTime() - lhsDate.getTime();
910+
}
911+
return 1;
912+
}
913+
}

src/ui/ToolchainSelection.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,8 @@ async function getQuickPickItems(
237237
});
238238

239239
// Find any Swift toolchains installed via Swiftly
240-
const swiftlyToolchains = (await Swiftly.list(logger))
241-
// Sort in descending order alphabetically
242-
.sort((a, b) => -a.localeCompare(b))
243-
.map<SwiftlyToolchainItem>(toolchainPath => ({
240+
const swiftlyToolchains = (await Swiftly.list(logger)).map<SwiftlyToolchainItem>(
241+
toolchainPath => ({
244242
type: "toolchain",
245243
label: path.basename(toolchainPath),
246244
category: "swiftly",
@@ -267,7 +265,8 @@ async function getQuickPickItems(
267265
);
268266
}
269267
},
270-
}));
268+
})
269+
);
271270

272271
if (activeToolchain) {
273272
const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged

src/utilities/version.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,8 @@ export class Version implements VersionInterface {
8787
isGreaterThanOrEqual(rhs: VersionInterface): boolean {
8888
return !this.isLessThan(rhs);
8989
}
90+
91+
compare(rhs: VersionInterface): number {
92+
return this.isGreaterThan(rhs) ? 1 : this.isLessThan(rhs) ? -1 : 0;
93+
}
9094
}

0 commit comments

Comments
 (0)