| @@ -9,6 +9,7 @@ · 8 unmodified lines |
| 9 | 9 | */ |
| 10 | 10 | import OS from "node:os"; |
| 11 | 11 | import { spawn as spawnNodeChildProcess } from "node:child_process"; |
| 12 | +import { pathToFileURL } from "node:url"; |
| 12 | 13 | |
| 13 | 14 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 14 | 15 | import { Array, Effect, FileSystem, Layer, Option, Path, Scope } from "effect"; |
| @@ -38,6 +39,7 @@ export interface DetachedSpawnInput { · 23 unmodified lines |
| 38 | 39 | readonly detached?: boolean; |
| 39 | 40 | readonly shell?: boolean; |
| 40 | 41 | readonly windowsVerbatimArguments?: boolean; |
| 42 | + readonly windowsHide?: boolean; |
| 41 | 43 | readonly stdin?: "ignore"; |
| 42 | 44 | readonly stdout?: "ignore"; |
| 43 | 45 | readonly stderr?: "ignore"; |
| @@ -119,6 +121,11 @@ const WSL_POWERSHELL_CANDIDATES = [ · 75 unmodified lines |
| 119 | 121 | "powershell.exe", |
| 120 | 122 | "pwsh.exe", |
| 121 | 123 | ] as const; |
| 124 | +const WINDOWS_EDITOR_URI_SCHEMES: Partial<Record<EditorId, string>> = { |
| 125 | + vscode: "vscode", |
| 126 | + "vscode-insiders": "vscode-insiders", |
| 127 | + vscodium: "vscodium", |
| 128 | +}; |
| 122 | 129 | |
| 123 | 130 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { |
| 124 | 131 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); |
| @@ -128,6 +135,24 @@ function stripWrappingQuotes(value: string): string { · 3 unmodified lines |
| 128 | 135 | return value.replace(/^"+|"+$/g, ""); |
| 129 | 136 | } |
| 130 | 137 | |
| 138 | +function splitLineColumnSuffix(target: string): { |
| 139 | + readonly filePath: string; |
| 140 | + readonly suffix: string; |
| 141 | +} { |
| 142 | + const match = target.match(LINE_COLUMN_SUFFIX_PATTERN); |
| 143 | + if (!match) { |
| 144 | + return { |
| 145 | + filePath: target, |
| 146 | + suffix: "", |
| 147 | + }; |
| 148 | + } |
| 149 | + |
| 150 | + return { |
| 151 | + filePath: target.slice(0, -match[0].length), |
| 152 | + suffix: match[0], |
| 153 | + }; |
| 154 | +} |
| 155 | + |
| 131 | 156 | function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { |
| 132 | 157 | return env.PATH ?? env.Path ?? env.path ?? ""; |
| 133 | 158 | } |
| @@ -188,6 +213,10 @@ function quotePowerShellValue(value: string): string { · 54 unmodified lines |
| 188 | 213 | return `'${value.replaceAll("'", "''")}'`; |
| 189 | 214 | } |
| 190 | 215 | |
| 216 | +function quotePowerShellArgument(value: string): string { |
| 217 | + return `"\`"${value.replaceAll("`", "``").replaceAll('"', '`"')}\`""`; |
| 218 | +} |
| 219 | + |
| 191 | 220 | function encodePowerShellCommand(command: string): string { |
| 192 | 221 | return Buffer.from(command, "utf16le").toString("base64"); |
| 193 | 222 | } |
| @@ -218,6 +247,19 @@ function shouldPreferWindowsOpenerOnWsl(input: OpenExternalInput, runtime: Launc · 24 unmodified lines |
| 218 | 247 | return runtime.isWsl && !runtime.isInsideContainer && isUriLikeTarget(input.target); |
| 219 | 248 | } |
| 220 | 249 | |
| 250 | +function makeWindowsEditorProtocolTarget(editor: EditorId, target: string): string | undefined { |
| 251 | + const scheme = WINDOWS_EDITOR_URI_SCHEMES[editor]; |
| 252 | + if (!scheme) return undefined; |
| 253 | + |
| 254 | + const { filePath, suffix } = splitLineColumnSuffix(target); |
| 255 | + const fileUrl = pathToFileURL(filePath).href; |
| 256 | + const fileTarget = fileUrl.startsWith("file:///") |
| 257 | + ? fileUrl.slice("file:///".length) |
| 258 | + : fileUrl.replace(/^file:\/\//, ""); |
| 259 | + |
| 260 | + return `${scheme}://file/${fileTarget}${suffix}`; |
| 261 | +} |
| 262 | + |
| 221 | 263 | function makeLaunchPlan( |
| 222 | 264 | command: string, |
| 223 | 265 | args: ReadonlyArray<string>, |
| @@ -309,8 +351,8 @@ function makePowerShellStartProcessArgs( · 85 unmodified lines |
| 309 | 351 | args: ReadonlyArray<string>, |
| 310 | 352 | ): ReadonlyArray<string> { |
| 311 | 353 | const argumentList = |
| 312 | | − args.length > 0 ? ` -ArgumentList ${args.map(quotePowerShellValue).join(", ")}` : ""; |
| 313 | | − const command = `Start-Process -FilePath ${quotePowerShellValue(commandPath)}${argumentList} -WindowStyle Hidden`; |
| 354 | + args.length > 0 ? ` -ArgumentList ${args.map(quotePowerShellArgument).join(",")}` : ""; |
| 355 | + const command = `Start ${quotePowerShellArgument(commandPath)}${argumentList} -WindowStyle Hidden`; |
| 314 | 356 | return [ |
| 315 | 357 | "-NoProfile", |
| 316 | 358 | "-NonInteractive", |
| @@ -336,6 +378,8 @@ function makeWindowsExplorerPlan(input: OpenExternalInput): LaunchPlan { · 19 unmodified lines |
| 336 | 378 | return makeLaunchPlan("explorer", [input.target], { |
| 337 | 379 | wait: false, |
| 338 | 380 | allowNonzeroExitCode: false, |
| 381 | + detached: true, |
| 382 | + stdio: "ignore", |
| 339 | 383 | shell: false, |
| 340 | 384 | }); |
| 341 | 385 | } |
| @@ -408,6 +452,9 @@ function resolveSpawnInput( · 66 unmodified lines |
| 408 | 452 | args: makePowerShellStartProcessArgs(resolvedCommand.path, plan.args), |
| 409 | 453 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), |
| 410 | 454 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), |
| 455 | + ...(runtime.platform === "win32" |
| 456 | + ? { windowsVerbatimArguments: true, windowsHide: true } |
| 457 | + : {}), |
| 411 | 458 | ...(plan.stdio === "ignore" |
| 412 | 459 | ? { |
| 413 | 460 | stdin: "ignore" as const, |
| @@ -417,6 +464,7 @@ function resolveSpawnInput( · 3 unmodified lines |
| 417 | 464 | : {}), |
| 418 | 465 | }; |
| 419 | 466 | } |
| 467 | + const windowsHide = runtime.platform === "win32" && plan.detached ? true : undefined; |
| 420 | 468 | return { |
| 421 | 469 | command: resolvedCommand.usesCmdWrapper |
| 422 | 470 | ? resolveWindowsCommandShell(runtime.env) |
| @@ -426,6 +474,7 @@ function resolveSpawnInput( · 3 unmodified lines |
| 426 | 474 | : [...plan.args], |
| 427 | 475 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), |
| 428 | 476 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), |
| 477 | + ...(windowsHide ? { windowsHide } : {}), |
| 429 | 478 | ...(resolvedCommand.usesCmdWrapper ? { windowsVerbatimArguments: true } : {}), |
| 430 | 479 | ...(plan.stdio === "ignore" |
| 431 | 480 | ? { |
| @@ -525,6 +574,7 @@ function defaultSpawnDetached( · 93 unmodified lines |
| 525 | 574 | spawnNodeChildProcess(input.command, [...input.args], { |
| 526 | 575 | ...(input.detached !== undefined ? { detached: input.detached } : {}), |
| 527 | 576 | ...(input.shell !== undefined ? { shell: input.shell } : {}), |
| 577 | + ...(input.windowsHide !== undefined ? { windowsHide: input.windowsHide } : {}), |
| 528 | 578 | ...(input.windowsVerbatimArguments !== undefined |
| 529 | 579 | ? { windowsVerbatimArguments: input.windowsVerbatimArguments } |
| 530 | 580 | : {}), |
| @@ -891,6 +941,21 @@ export const make = Effect.fn("makeDesktopLauncher")(function* ( · 360 unmodified lines |
| 891 | 941 | return yield* new DesktopLauncherUnknownEditorError({ editor: input.editor }); |
| 892 | 942 | } |
| 893 | 943 | |
| 944 | + const windowsEditorProtocolTarget = |
| 945 | + runtime.platform === "win32" |
| 946 | + ? makeWindowsEditorProtocolTarget(input.editor, input.cwd) |
| 947 | + : undefined; |
| 948 | + if (windowsEditorProtocolTarget) { |
| 949 | + return yield* runFirstAvailablePlan( |
| 950 | + [makeWindowsExplorerPlan({ target: windowsEditorProtocolTarget })], |
| 951 | + { |
| 952 | + operation: "openInEditor", |
| 953 | + target: input.cwd, |
| 954 | + editor: input.editor, |
| 955 | + }, |
| 956 | + ); |
| 957 | + } |
| 958 | + |
| 894 | 959 | if (editor.command) { |
| 895 | 960 | return yield* runFirstAvailablePlan( |
| 896 | 961 | [ |