| ... | @@ -1041,6 +1041,136 @@ for (const { platform, installPath, editor, args } of [ | ... | @@ -1041,6 +1041,136 @@ for (const { platform, installPath, editor, args } of [ |
| 1041 | ); | 1041 | ); |
| 1042 | } | 1042 | } |
| 1043 | | 1043 | |
| | 1044 | // JetBrains Toolbox names its bundles after the app and the installed version |
| | 1045 | // (`IntelliJ IDEA 2026.1.4.app`), so the darwin branch has to list the |
| | 1046 | // Applications directories the way the win32 branch does instead of probing |
| | 1047 | // exact `<name>.app` paths. The fixtures only use the mocked platform and a |
| | 1048 | // temp directory, so this runs on a Windows host too. |
| | 1049 | it.effect.each([ |
| | 1050 | { editor: "webstorm", installPath: "Applications/WebStorm 2026.2.app/Contents/MacOS/webstorm" }, |
| | 1051 | { editor: "idea", installPath: "Applications/IntelliJ IDEA 2026.1.4.app/Contents/MacOS/idea" }, |
| | 1052 | ] as const)( |
| | 1053 | "discovers and launches $editor from a versioned macOS bundle", |
| | 1054 | ({ editor, installPath }) => |
| | 1055 | Effect.gen(function* () { |
| | 1056 | const fs = yield* FileSystem.FileSystem; |
| | 1057 | const path = yield* Path.Path; |
| | 1058 | const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor-versioned-" }); |
| | 1059 | const executable = path.join(home, installPath); |
| | 1060 | yield* fs.makeDirectory(path.dirname(executable), { recursive: true }); |
| | 1061 | yield* fs.writeFileString(executable, "#!/bin/sh\n"); |
| | 1062 | yield* fs.chmod(executable, 0o755); |
| | 1063 | |
| | 1064 | let spawned: ChildProcess.StandardCommand | undefined; |
| | 1065 | yield* Effect.gen(function* () { |
| | 1066 | const launcher = yield* ExternalLauncher.ExternalLauncher; |
| | 1067 | assert.include(yield* launcher.resolveAvailableEditors(), editor); |
| | 1068 | yield* launcher.launchEditor({ editor, cwd: "/workspace with spaces/file.ts:12:4" }); |
| | 1069 | }).pipe( |
| | 1070 | Effect.provide( |
| | 1071 | testLayer({ |
| | 1072 | platform: "darwin", |
| | 1073 | env: { HOME: home, PATH: path.join(home, "empty") }, |
| | 1074 | onSpawn: (command) => { |
| | 1075 | spawned = command; |
| | 1076 | }, |
| | 1077 | }), |
| | 1078 | ), |
| | 1079 | ); |
| | 1080 | |
| | 1081 | assert.ok(spawned); |
| | 1082 | assert.equal(spawned.command, executable); |
| | 1083 | assert.deepEqual(spawned.args, [ |
| | 1084 | "--line", |
| | 1085 | "12", |
| | 1086 | "--column", |
| | 1087 | "4", |
| | 1088 | "/workspace with spaces/file.ts", |
| | 1089 | ]); |
| | 1090 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| | 1091 | ); |
| | 1092 | |
| | 1093 | // An app whose name merely starts with the same letters is a different IDE, and |
| | 1094 | // an exact `<name>.app` bundle keeps resolving the way it did before. |
| | 1095 | it.effect("matches macOS bundles on the name boundary only", () => |
| | 1096 | Effect.gen(function* () { |
| | 1097 | const fs = yield* FileSystem.FileSystem; |
| | 1098 | const path = yield* Path.Path; |
| | 1099 | const unrelatedHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor-unrelated-" }); |
| | 1100 | const unrelated = path.join( |
| | 1101 | unrelatedHome, |
| | 1102 | "Applications/WebStormHelper.app/Contents/MacOS/webstorm", |
| | 1103 | ); |
| | 1104 | yield* fs.makeDirectory(path.dirname(unrelated), { recursive: true }); |
| | 1105 | yield* fs.writeFileString(unrelated, "#!/bin/sh\n"); |
| | 1106 | yield* fs.chmod(unrelated, 0o755); |
| | 1107 | |
| | 1108 | const editorsFor = (home: string) => |
| | 1109 | Effect.gen(function* () { |
| | 1110 | const launcher = yield* ExternalLauncher.ExternalLauncher; |
| | 1111 | return yield* launcher.resolveAvailableEditors(); |
| | 1112 | }).pipe( |
| | 1113 | Effect.provide( |
| | 1114 | testLayer({ platform: "darwin", env: { HOME: home, PATH: path.join(home, "bin") } }), |
| | 1115 | ), |
| | 1116 | ); |
| | 1117 | |
| | 1118 | assert.notInclude(yield* editorsFor(unrelatedHome), "webstorm"); |
| | 1119 | |
| | 1120 | const exactHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor-exact-" }); |
| | 1121 | const exact = path.join(exactHome, "Applications/WebStorm.app/Contents/MacOS/webstorm"); |
| | 1122 | yield* fs.makeDirectory(path.dirname(exact), { recursive: true }); |
| | 1123 | yield* fs.writeFileString(exact, "#!/bin/sh\n"); |
| | 1124 | yield* fs.chmod(exact, 0o755); |
| | 1125 | |
| | 1126 | assert.include(yield* editorsFor(exactHome), "webstorm"); |
| | 1127 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| | 1128 | ); |
| | 1129 | |
| | 1130 | // `Visual Studio Code - Insiders.app` starts with the stable VS Code install |
| | 1131 | // name, so a plain prefix match let the stable editor resolve to the Insiders |
| | 1132 | // bundle. Insiders has to stay reachable under its own name, stable VS Code has |
| | 1133 | // to stay unresolved. |
| | 1134 | it.effect("keeps the macOS VS Code Insiders bundle out of stable VS Code", () => |
| | 1135 | Effect.gen(function* () { |
| | 1136 | const fs = yield* FileSystem.FileSystem; |
| | 1137 | const path = yield* Path.Path; |
| | 1138 | const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editor-insiders-" }); |
| | 1139 | // The real Insiders bundle ships both `code-insiders` and `code`. |
| | 1140 | const bin = path.join( |
| | 1141 | home, |
| | 1142 | "Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin", |
| | 1143 | ); |
| | 1144 | yield* fs.makeDirectory(bin, { recursive: true }); |
| | 1145 | for (const name of ["code", "code-insiders"]) { |
| | 1146 | yield* fs.writeFileString(path.join(bin, name), "#!/bin/sh\n"); |
| | 1147 | yield* fs.chmod(path.join(bin, name), 0o755); |
| | 1148 | } |
| | 1149 | |
| | 1150 | let spawned: ChildProcess.StandardCommand | undefined; |
| | 1151 | const editors = yield* Effect.gen(function* () { |
| | 1152 | const launcher = yield* ExternalLauncher.ExternalLauncher; |
| | 1153 | const available = yield* launcher.resolveAvailableEditors(); |
| | 1154 | yield* launcher.launchEditor({ editor: "vscode-insiders", cwd: "/workspace/file.ts" }); |
| | 1155 | return available; |
| | 1156 | }).pipe( |
| | 1157 | Effect.provide( |
| | 1158 | testLayer({ |
| | 1159 | platform: "darwin", |
| | 1160 | env: { HOME: home, PATH: path.join(home, "empty") }, |
| | 1161 | onSpawn: (command) => { |
| | 1162 | spawned = command; |
| | 1163 | }, |
| | 1164 | }), |
| | 1165 | ), |
| | 1166 | ); |
| | 1167 | |
| | 1168 | assert.notInclude(editors, "vscode"); |
| | 1169 | assert.include(editors, "vscode-insiders"); |
| | 1170 | assert.equal(spawned?.command, path.join(bin, "code-insiders")); |
| | 1171 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| | 1172 | ); |
| | 1173 | |
| 1044 | // `agy` is the standalone Antigravity CLI, which installs to ~/.local/bin on | 1174 | // `agy` is the standalone Antigravity CLI, which installs to ~/.local/bin on |
| 1045 | // macOS and Linux and to its own bin folder on Windows. It is not the IDE. | 1175 | // macOS and Linux and to its own bin folder on Windows. It is not the IDE. |
| 1046 | for (const { platform, installPath, onPath } of [ | 1176 | for (const { platform, installPath, onPath } of [ |