| ... | @@ -7,7 +7,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -7,7 +7,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 7 | import { assert, it } from "@effect/vitest"; | 7 | import { assert, it } from "@effect/vitest"; |
| 8 | import * as ConfigProvider from "effect/ConfigProvider"; | 8 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 9 | import * as Deferred from "effect/Deferred"; | 9 | import * as Deferred from "effect/Deferred"; |
| | 10 | import * as Cause from "effect/Cause"; |
| 10 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| | 12 | import * as Exit from "effect/Exit"; |
| 11 | import * as Fiber from "effect/Fiber"; | 13 | import * as Fiber from "effect/Fiber"; |
| 12 | import * as FileSystem from "effect/FileSystem"; | 14 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Layer from "effect/Layer"; | 15 | import * as Layer from "effect/Layer"; |
| ... | @@ -1381,6 +1383,93 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan", | ... | @@ -1381,6 +1383,93 @@ it.effect("keeps scanning after the caller is interrupted and shares that scan", |
| 1381 | ); | 1383 | ); |
| 1382 | }); | 1384 | }); |
| 1383 | | 1385 | |
| | 1386 | const launchWindowsShimEditor = (cwd: string) => |
| | 1387 | Effect.gen(function* () { |
| | 1388 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1389 | const path = yield* Path.Path; |
| | 1390 | const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" }); |
| | 1391 | yield* fileSystem.writeFileString(path.join(binDir, "code.CMD"), "@echo off\r\n"); |
| | 1392 | let spawned: ChildProcess.StandardCommand | undefined; |
| | 1393 | const exit = yield* Effect.gen(function* () { |
| | 1394 | const launcher = yield* ExternalLauncher.ExternalLauncher; |
| | 1395 | return yield* launcher.launchEditor({ editor: "vscode", cwd }).pipe(Effect.exit); |
| | 1396 | }).pipe( |
| | 1397 | Effect.provide( |
| | 1398 | layerTest({ |
| | 1399 | platform: "win32", |
| | 1400 | env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" }, |
| | 1401 | resolveExecutable: (command) => |
| | 1402 | command === "code" ? "C:\\Program Files\\Microsoft VS Code\\bin\\code.CMD" : command, |
| | 1403 | onSpawn: (command) => { |
| | 1404 | spawned = command; |
| | 1405 | }, |
| | 1406 | }), |
| | 1407 | ), |
| | 1408 | ); |
| | 1409 | return { exit, spawned }; |
| | 1410 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)); |
| | 1411 | |
| | 1412 | it.effect.each([ |
| | 1413 | ["CRLF", "C:\\workspace\\file.ts\r\ncalc.exe"], |
| | 1414 | ["LF", "C:\\workspace\\file.ts\ncalc.exe"], |
| | 1415 | ["double quote", 'C:\\workspace\\file.ts" & calc.exe & "'], |
| | 1416 | ] as const)("does not start a Windows command shim for a path with a $0", ([, cwd]) => |
| | 1417 | Effect.gen(function* () { |
| | 1418 | const { exit, spawned } = yield* launchWindowsShimEditor(cwd); |
| | 1419 | assert.equal(spawned, undefined); |
| | 1420 | assert.isTrue(Exit.isFailure(exit)); |
| | 1421 | if (Exit.isFailure(exit)) { |
| | 1422 | assert.instanceOf( |
| | 1423 | Cause.squash(exit.cause), |
| | 1424 | ExternalLauncher.ExternalLauncherUnsupportedTargetError, |
| | 1425 | ); |
| | 1426 | } |
| | 1427 | }), |
| | 1428 | ); |
| | 1429 | |
| | 1430 | it.effect("starts a Windows command shim for unicode paths with cmd metacharacters", () => |
| | 1431 | Effect.gen(function* () { |
| | 1432 | const { exit, spawned } = yield* launchWindowsShimEditor( |
| | 1433 | "C:\\Users\\jö\\R&D (100%)\\naïve ^file!.ts:3:7", |
| | 1434 | ); |
| | 1435 | assert.isTrue(Exit.isSuccess(exit)); |
| | 1436 | assert.ok(spawned); |
| | 1437 | assert.equal(spawned.options.shell, true); |
| | 1438 | assert.deepEqual(spawned.args, [ |
| | 1439 | '^"--goto^"', |
| | 1440 | '^"C:\\Users\\jö\\R^&D^ ^(100^%^)\\naïve^ ^^file^!.ts:3:7^"', |
| | 1441 | ]); |
| | 1442 | }), |
| | 1443 | ); |
| | 1444 | |
| | 1445 | it.effect.skipIf(windowsHost)("passes line breaks through to editors started without a shell", () => |
| | 1446 | Effect.gen(function* () { |
| | 1447 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1448 | const path = yield* Path.Path; |
| | 1449 | const binDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-editors-" }); |
| | 1450 | const zed = path.join(binDir, "zed"); |
| | 1451 | yield* fileSystem.writeFileString(zed, "#!/bin/sh\n"); |
| | 1452 | yield* fileSystem.chmod(zed, 0o755); |
| | 1453 | let spawned: ChildProcess.StandardCommand | undefined; |
| | 1454 | yield* Effect.gen(function* () { |
| | 1455 | const launcher = yield* ExternalLauncher.ExternalLauncher; |
| | 1456 | yield* launcher.launchEditor({ editor: "zed", cwd: "/workspace/odd\nname.ts" }); |
| | 1457 | }).pipe( |
| | 1458 | Effect.provide( |
| | 1459 | layerTest({ |
| | 1460 | platform: "linux", |
| | 1461 | env: { PATH: binDir, HOME: binDir }, |
| | 1462 | onSpawn: (command) => { |
| | 1463 | spawned = command; |
| | 1464 | }, |
| | 1465 | }), |
| | 1466 | ), |
| | 1467 | ); |
| | 1468 | assert.ok(spawned); |
| | 1469 | assert.deepEqual(spawned.args, ["/workspace/odd\nname.ts"]); |
| | 1470 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| | 1471 | ); |
| | 1472 | |
| 1384 | it.effect("rejects unknown editors through the service API", () => | 1473 | it.effect("rejects unknown editors through the service API", () => |
| 1385 | Effect.gen(function* () { | 1474 | Effect.gen(function* () { |
| 1386 | const launcher = yield* ExternalLauncher.ExternalLauncher; | 1475 | const launcher = yield* ExternalLauncher.ExternalLauncher; |