| @@ -9,6 +9,7 @@ · 8 unmodified lines |
| 9 | */ | 9 | */ |
| 10 | import OS from "node:os"; | 10 | import OS from "node:os"; |
| 11 | import { spawn as spawnNodeChildProcess } from "node:child_process"; | 11 | import { spawn as spawnNodeChildProcess } from "node:child_process"; |
| | 12 | import { pathToFileURL } from "node:url"; |
| 12 | | 13 | |
| 13 | import { EDITORS, type EditorId } from "@t3tools/contracts"; | 14 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 14 | import { Array, Effect, FileSystem, Layer, Option, Path, Scope } from "effect"; | 15 | import { Array, Effect, FileSystem, Layer, Option, Path, Scope } from "effect"; |
| @@ -38,6 +39,7 @@ export interface DetachedSpawnInput { · 23 unmodified lines |
| 38 | readonly detached?: boolean; | 39 | readonly detached?: boolean; |
| 39 | readonly shell?: boolean; | 40 | readonly shell?: boolean; |
| 40 | readonly windowsVerbatimArguments?: boolean; | 41 | readonly windowsVerbatimArguments?: boolean; |
| | 42 | readonly windowsHide?: boolean; |
| 41 | readonly stdin?: "ignore"; | 43 | readonly stdin?: "ignore"; |
| 42 | readonly stdout?: "ignore"; | 44 | readonly stdout?: "ignore"; |
| 43 | readonly stderr?: "ignore"; | 45 | readonly stderr?: "ignore"; |
| @@ -119,6 +121,11 @@ const WSL_POWERSHELL_CANDIDATES = [ · 75 unmodified lines |
| 119 | "powershell.exe", | 121 | "powershell.exe", |
| 120 | "pwsh.exe", | 122 | "pwsh.exe", |
| 121 | ] as const; | 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 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { | 130 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { |
| 124 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); | 131 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); |
| @@ -128,6 +135,24 @@ function stripWrappingQuotes(value: string): string { · 3 unmodified lines |
| 128 | return value.replace(/^"+|"+$/g, ""); | 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 | function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { | 156 | function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { |
| 132 | return env.PATH ?? env.Path ?? env.path ?? ""; | 157 | return env.PATH ?? env.Path ?? env.path ?? ""; |
| 133 | } | 158 | } |
| @@ -188,6 +213,10 @@ function quotePowerShellValue(value: string): string { · 54 unmodified lines |
| 188 | return `'${value.replaceAll("'", "''")}'`; | 213 | return `'${value.replaceAll("'", "''")}'`; |
| 189 | } | 214 | } |
| 190 | | 215 | |
| | 216 | function quotePowerShellArgument(value: string): string { |
| | 217 | return `"\`"${value.replaceAll("`", "``").replaceAll('"', '`"')}\`""`; |
| | 218 | } |
| | 219 | |
| 191 | function encodePowerShellCommand(command: string): string { | 220 | function encodePowerShellCommand(command: string): string { |
| 192 | return Buffer.from(command, "utf16le").toString("base64"); | 221 | return Buffer.from(command, "utf16le").toString("base64"); |
| 193 | } | 222 | } |
| @@ -218,6 +247,19 @@ function shouldPreferWindowsOpenerOnWsl(input: OpenExternalInput, runtime: Launc · 24 unmodified lines |
| 218 | return runtime.isWsl && !runtime.isInsideContainer && isUriLikeTarget(input.target); | 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 | function makeLaunchPlan( | 263 | function makeLaunchPlan( |
| 222 | command: string, | 264 | command: string, |
| 223 | args: ReadonlyArray<string>, | 265 | args: ReadonlyArray<string>, |
| @@ -309,8 +351,8 @@ function makePowerShellStartProcessArgs( · 85 unmodified lines |
| 309 | args: ReadonlyArray<string>, | 351 | args: ReadonlyArray<string>, |
| 310 | ): ReadonlyArray<string> { | 352 | ): ReadonlyArray<string> { |
| 311 | const argumentList = | 353 | const argumentList = |
| 312 | args.length > 0 ? ` -ArgumentList ${args.map(quotePowerShellValue).join(", ")}` : ""; | 354 | args.length > 0 ? ` -ArgumentList ${args.map(quotePowerShellArgument).join(",")}` : ""; |
| 313 | const command = `Start-Process -FilePath ${quotePowerShellValue(commandPath)}${argumentList} -WindowStyle Hidden`; | 355 | const command = `Start ${quotePowerShellArgument(commandPath)}${argumentList} -WindowStyle Hidden`; |
| 314 | return [ | 356 | return [ |
| 315 | "-NoProfile", | 357 | "-NoProfile", |
| 316 | "-NonInteractive", | 358 | "-NonInteractive", |
| @@ -336,6 +378,8 @@ function makeWindowsExplorerPlan(input: OpenExternalInput): LaunchPlan { · 19 unmodified lines |
| 336 | return makeLaunchPlan("explorer", [input.target], { | 378 | return makeLaunchPlan("explorer", [input.target], { |
| 337 | wait: false, | 379 | wait: false, |
| 338 | allowNonzeroExitCode: false, | 380 | allowNonzeroExitCode: false, |
| | 381 | detached: true, |
| | 382 | stdio: "ignore", |
| 339 | shell: false, | 383 | shell: false, |
| 340 | }); | 384 | }); |
| 341 | } | 385 | } |
| @@ -408,6 +452,9 @@ function resolveSpawnInput( · 66 unmodified lines |
| 408 | args: makePowerShellStartProcessArgs(resolvedCommand.path, plan.args), | 452 | args: makePowerShellStartProcessArgs(resolvedCommand.path, plan.args), |
| 409 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), | 453 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), |
| 410 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), | 454 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), |
| | 455 | ...(runtime.platform === "win32" |
| | 456 | ? { windowsVerbatimArguments: true, windowsHide: true } |
| | 457 | : {}), |
| 411 | ...(plan.stdio === "ignore" | 458 | ...(plan.stdio === "ignore" |
| 412 | ? { | 459 | ? { |
| 413 | stdin: "ignore" as const, | 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 | return { | 468 | return { |
| 421 | command: resolvedCommand.usesCmdWrapper | 469 | command: resolvedCommand.usesCmdWrapper |
| 422 | ? resolveWindowsCommandShell(runtime.env) | 470 | ? resolveWindowsCommandShell(runtime.env) |
| @@ -426,6 +474,7 @@ function resolveSpawnInput( · 3 unmodified lines |
| 426 | : [...plan.args], | 474 | : [...plan.args], |
| 427 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), | 475 | ...(plan.detached !== undefined ? { detached: plan.detached } : {}), |
| 428 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), | 476 | ...(plan.shell !== undefined ? { shell: plan.shell } : {}), |
| | 477 | ...(windowsHide ? { windowsHide } : {}), |
| 429 | ...(resolvedCommand.usesCmdWrapper ? { windowsVerbatimArguments: true } : {}), | 478 | ...(resolvedCommand.usesCmdWrapper ? { windowsVerbatimArguments: true } : {}), |
| 430 | ...(plan.stdio === "ignore" | 479 | ...(plan.stdio === "ignore" |
| 431 | ? { | 480 | ? { |
| @@ -525,6 +574,7 @@ function defaultSpawnDetached( · 93 unmodified lines |
| 525 | spawnNodeChildProcess(input.command, [...input.args], { | 574 | spawnNodeChildProcess(input.command, [...input.args], { |
| 526 | ...(input.detached !== undefined ? { detached: input.detached } : {}), | 575 | ...(input.detached !== undefined ? { detached: input.detached } : {}), |
| 527 | ...(input.shell !== undefined ? { shell: input.shell } : {}), | 576 | ...(input.shell !== undefined ? { shell: input.shell } : {}), |
| | 577 | ...(input.windowsHide !== undefined ? { windowsHide: input.windowsHide } : {}), |
| 528 | ...(input.windowsVerbatimArguments !== undefined | 578 | ...(input.windowsVerbatimArguments !== undefined |
| 529 | ? { windowsVerbatimArguments: input.windowsVerbatimArguments } | 579 | ? { windowsVerbatimArguments: input.windowsVerbatimArguments } |
| 530 | : {}), | 580 | : {}), |
| @@ -891,6 +941,21 @@ export const make = Effect.fn("makeDesktopLauncher")(function* ( · 360 unmodified lines |
| 891 | return yield* new DesktopLauncherUnknownEditorError({ editor: input.editor }); | 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 | if (editor.command) { | 959 | if (editor.command) { |
| 895 | return yield* runFirstAvailablePlan( | 960 | return yield* runFirstAvailablePlan( |
| 896 | [ | 961 | [ |