diff --git a/src/features/run/rubyTypes.ts b/src/features/run/rubyTypes.ts index 842845c80..fb22e79b7 100644 --- a/src/features/run/rubyTypes.ts +++ b/src/features/run/rubyTypes.ts @@ -10,12 +10,27 @@ import { commandName } from "./commandName"; const on = (v: string | undefined) => v === "true"; +const RUBY_TOOLS = new Set(["ruby", "bundle", "rake", "rspec"]); + +/** + * The launcher for `want` (`bundle` or the tool itself) from the Launcher field. A field holding + * the other Ruby tool of the same interpreter (`/v/bin/ruby` when Bundler is ticked) means its + * sibling in that directory; anything else (a binstub, `spring`) is the user's own and stays. + */ +function launcher(command: string | undefined, want: string): string { + const given = command?.trim(); + if (!given) return want; + const current = commandName(given); + if (current === want || !RUBY_TOOLS.has(current)) return given; + const dir = /^.*[/\\]/.exec(given)?.[0] ?? ""; + return dir + want + (/\.exe$/i.test(given) ? ".exe" : ""); +} + /** `bundle exec ` or ``, launched through `params.command` when one is set. */ function tool(p: Readonly>, name: string): { command: string; args: string[] } { - const command = p.command?.trim(); return on(p.bundler) - ? { command: command || "bundle", args: ["exec", name] } - : { command: command || name, args: [] }; + ? { command: launcher(p.command, "bundle"), args: ["exec", name] } + : { command: launcher(p.command, name), args: [] }; } /** Splits `bundle exec rest…` / ` rest…` into the launcher, the Bundler flag and `rest`. */ diff --git a/src/features/run/runConfigTypes.test.tsx b/src/features/run/runConfigTypes.test.tsx index e8b412abf..bdff433c7 100644 --- a/src/features/run/runConfigTypes.test.tsx +++ b/src/features/run/runConfigTypes.test.tsx @@ -225,6 +225,24 @@ describe("configuration types", () => { }); }); + it("ticking Bundler swaps the interpreter launcher for its bundle sibling, and back", () => { + const abs = "/v/ruby/bin/ruby"; + const run = cfg("r", { kind: "ruby", command: abs, args: ["-Ilib", "test/a_test.rb"] }); + expect(withParams(run, { ...formParams(run)!, bundler: "true" })).toMatchObject({ + command: "/v/ruby/bin/bundle", + args: ["exec", "ruby", "-Ilib", "test/a_test.rb"], + }); + const bundled = cfg("r", { kind: "ruby", command: "/v/ruby/bin/bundle", args: ["exec", "ruby", "a.rb"] }); + expect(withParams(bundled, { ...formParams(bundled)!, bundler: "" })).toMatchObject({ + command: "/v/ruby/bin/ruby", + args: ["a.rb"], + }); + // A launcher of the user's own (binstub, spring) is kept. + expect(withParams(run, { ...formParams(run)!, bundler: "true", command: "bin/spring" })).toMatchObject({ + command: "bin/spring", + }); + }); + it("ruby types keep option values with their options and the argument order as written", () => { const ruby = (args: string[]) => formParams(cfg("r", { kind: "ruby", command: "ruby", args })); expect(ruby(["-I", "lib", "-w", "bin/app", "--port", "3000"])).toEqual({