| 1 | export interface WslUncPath { |
| 2 | readonly distro: string; |
| 3 | readonly linuxPath: string; |
| 4 | } |
| 5 | |
| 6 | export interface WslEnvironmentCandidate<TEnvironmentId extends string = string> { |
| 7 | readonly environmentId: TEnvironmentId; |
| 8 | readonly backendId: string; |
| 9 | readonly runningDistro: string | null; |
| 10 | } |
| 11 | |
| 12 | export interface WslEnvironmentConfiguration { |
| 13 | readonly enabled: boolean; |
| 14 | readonly wslOnly: boolean; |
| 15 | readonly distro: string | null; |
| 16 | readonly distros: ReadonlyArray<{ |
| 17 | readonly name: string; |
| 18 | readonly isDefault: boolean; |
| 19 | }>; |
| 20 | } |
| 21 | |
| 22 | export interface WslProjectSelection<TEnvironmentId extends string = string> extends WslUncPath { |
| 23 | readonly environmentId: TEnvironmentId; |
| 24 | } |
| 25 | |
| 26 | const WSL_UNC_PREFIXES = ["\\\\wsl.localhost\\", "\\\\wsl$\\"] as const; |
| 27 | const WSL_DISTRO_NAME_PATTERN = /^[A-Za-z0-9._-]+$/; |
| 28 | const WSL_DEFAULT_BACKEND_ID = "wsl:default"; |
| 29 | |
| 30 | export function parseWslUncPath(input: string): WslUncPath | null { |
| 31 | const normalized = input.trim().replaceAll("/", "\\"); |
| 32 | const prefix = WSL_UNC_PREFIXES.find((candidate) => |
| 33 | normalized.toLowerCase().startsWith(candidate.toLowerCase()), |
| 34 | ); |
| 35 | if (!prefix) { |
| 36 | return null; |
| 37 | } |
| 38 | |
| 39 | const rest = normalized.slice(prefix.length); |
| 40 | const segments = rest.split("\\").filter((segment) => segment.length > 0); |
| 41 | const distro = segments.shift(); |
| 42 | if (!distro || !WSL_DISTRO_NAME_PATTERN.test(distro)) { |
| 43 | return null; |
| 44 | } |
| 45 | |
| 46 | return { |
| 47 | distro, |
| 48 | linuxPath: segments.length === 0 ? "/" : `/${segments.join("/")}`, |
| 49 | }; |
| 50 | } |
| 51 | |
| 52 | export function resolveWslProjectSelection<TEnvironmentId extends string>( |
| 53 | input: string, |
| 54 | candidates: ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>>, |
| 55 | ): WslProjectSelection<TEnvironmentId> | null { |
| 56 | const parsed = parseWslUncPath(input); |
| 57 | if (!parsed) { |
| 58 | return null; |
| 59 | } |
| 60 | |
| 61 | const exact = candidates.find((candidate) => { |
| 62 | if (!candidate.backendId.startsWith("wsl:")) { |
| 63 | return false; |
| 64 | } |
| 65 | |
| 66 | const backendDistro = candidate.backendId.slice("wsl:".length); |
| 67 | const runningDistro = |
| 68 | candidate.runningDistro ?? |
| 69 | (backendDistro.length > 0 && backendDistro.toLowerCase() !== "default" |
| 70 | ? backendDistro |
| 71 | : null); |
| 72 | return runningDistro?.toLowerCase() === parsed.distro.toLowerCase(); |
| 73 | }); |
| 74 | return exact ? { ...parsed, environmentId: exact.environmentId } : null; |
| 75 | } |
| 76 | |
| 77 | interface ConfiguredWslBackend { |
| 78 | readonly backendId: string; |
| 79 | readonly runningDistro: string | null; |
| 80 | } |
| 81 | |
| 82 | function resolveConfiguredWslBackend( |
| 83 | configuration: WslEnvironmentConfiguration | null, |
| 84 | ): ConfiguredWslBackend | null { |
| 85 | if (!configuration) { |
| 86 | return null; |
| 87 | } |
| 88 | |
| 89 | if (configuration.distro === null) { |
| 90 | return { backendId: WSL_DEFAULT_BACKEND_ID, runningDistro: null }; |
| 91 | } |
| 92 | |
| 93 | const configuredDistro = configuration.distro.trim(); |
| 94 | if (configuredDistro.length === 0) { |
| 95 | return null; |
| 96 | } |
| 97 | |
| 98 | const installedDistro = configuration.distros.find( |
| 99 | (distro) => distro.name.toLowerCase() === configuredDistro.toLowerCase(), |
| 100 | ); |
| 101 | const resolvedDistro = installedDistro?.name ?? configuredDistro; |
| 102 | return { backendId: `wsl:${resolvedDistro}`, runningDistro: resolvedDistro }; |
| 103 | } |
| 104 | |
| 105 | export function resolveProjectPickerTarget<TEnvironmentId extends string>(input: { |
| 106 | readonly browseEnvironmentId: TEnvironmentId | null; |
| 107 | readonly primaryEnvironmentId: TEnvironmentId | null; |
| 108 | readonly desktopInstanceId: string | null; |
| 109 | readonly wslConfiguration: WslEnvironmentConfiguration | null; |
| 110 | }): string | null { |
| 111 | if (input.desktopInstanceId !== null) { |
| 112 | return input.desktopInstanceId; |
| 113 | } |
| 114 | |
| 115 | if ( |
| 116 | input.browseEnvironmentId === null || |
| 117 | input.browseEnvironmentId !== input.primaryEnvironmentId || |
| 118 | !input.wslConfiguration?.enabled || |
| 119 | !input.wslConfiguration.wslOnly |
| 120 | ) { |
| 121 | return null; |
| 122 | } |
| 123 | |
| 124 | return resolveConfiguredWslBackend(input.wslConfiguration)?.backendId ?? null; |
| 125 | } |
| 126 | |
| 127 | export function applyWslEnvironmentConfiguration<TEnvironmentId extends string>( |
| 128 | candidates: ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>>, |
| 129 | primaryEnvironmentId: TEnvironmentId | null, |
| 130 | configuration: WslEnvironmentConfiguration | null, |
| 131 | primaryRunningDistro: string | null = null, |
| 132 | ): ReadonlyArray<WslEnvironmentCandidate<TEnvironmentId>> { |
| 133 | if (!configuration) { |
| 134 | return candidates; |
| 135 | } |
| 136 | |
| 137 | const configuredBackend = resolveConfiguredWslBackend(configuration); |
| 138 | if (!configuredBackend) { |
| 139 | return candidates; |
| 140 | } |
| 141 | |
| 142 | if ( |
| 143 | configuration.enabled && |
| 144 | configuration.wslOnly && |
| 145 | primaryEnvironmentId !== null && |
| 146 | !candidates.some((candidate) => candidate.environmentId === primaryEnvironmentId) |
| 147 | ) { |
| 148 | return [ |
| 149 | ...candidates, |
| 150 | { |
| 151 | environmentId: primaryEnvironmentId, |
| 152 | ...configuredBackend, |
| 153 | runningDistro: primaryRunningDistro ?? configuredBackend.runningDistro, |
| 154 | }, |
| 155 | ]; |
| 156 | } |
| 157 | |
| 158 | return candidates; |
| 159 | } |