diff --git a/cli/lib/devcontainer.bash b/cli/lib/devcontainer.bash index 90b873d9..1b775c72 100644 --- a/cli/lib/devcontainer.bash +++ b/cli/lib/devcontainer.bash @@ -72,6 +72,58 @@ apply_ide_customizations() { mv "$tmpfile" "$file" } +# Adds JetBrains Marketplace plugins for selected stacks to devcontainer.json. +# Replaces the empty `"plugins": []` inside the JetBrains customizations +# block that apply_ide_customizations emits. Symmetric counterpart to +# customize_devcontainer_extensions() for the VS Code path. +# +# Silently no-ops when no stack contributes a JetBrains plugin (e.g. stacks +# where language support is bundled in the IDE) — the empty array stays. +# +# Args: +# $1 - Path to the devcontainer.json file +# $@ - Stack names (remaining args) +customize_devcontainer_plugins() { + local devcontainer_json=$1 + shift + + local plugin_ids=() + local stack plugin + for stack in "$@"; do + plugin=$(stack_jetbrains_plugin "$stack") + if [[ -n "$plugin" ]]; then + plugin_ids+=("$plugin") + fi + done + + if [[ ${#plugin_ids[@]} -eq 0 ]]; then + return 0 + fi + + # Build the JSON array literal: "id1", "id2", "id3" + local joined="" + local id + for id in "${plugin_ids[@]}"; do + if [[ -n "$joined" ]]; then + joined+=", " + fi + joined+="\"${id}\"" + done + + # Rewrite `"plugins": []` in place. The literal is uniquely emitted by + # apply_ide_customizations, so a simple line rewrite is safe here. + local tmpfile="${devcontainer_json}.tmp" + local line + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == *'"plugins": []'* ]]; then + printf '%s\n' "${line//\"plugins\": \[\]/\"plugins\": [${joined}]}" + else + printf '%s\n' "$line" + fi + done < "$devcontainer_json" > "$tmpfile" + mv "$tmpfile" "$devcontainer_json" +} + # Adds VS Code extensions for selected stacks to devcontainer.json. # Replaces the // __STACK_EXTENSIONS__ placeholder line. # Args: diff --git a/cli/lib/stacks.bash b/cli/lib/stacks.bash index eb39da32..88905427 100644 --- a/cli/lib/stacks.bash +++ b/cli/lib/stacks.bash @@ -60,6 +60,37 @@ stack_env_entries() { esac } +# Returns the JetBrains Marketplace plugin ID for a stack (empty if none). +# Symmetric to stack_extension() for the JetBrains devcontainer path. +# Gateway installs entries listed here into the backend IDE at start-up. +# +# Notes on editions and bundling: +# - node/java: language support is bundled in IntelliJ IDEA (Community +# and Ultimate). No plugin needed. +# - python: PythonCore is the free plugin that ships with PyCharm +# Community and works in IntelliJ IDEA Community as well. +# - go/ruby/nodejs-advanced: JetBrains gates these behind Ultimate. +# The plugin IDs below install correctly only when the backend is +# an Ultimate build (or the language-specific IDE like GoLand, +# RubyMine, WebStorm). On Community backends Gateway silently skips +# them. +# - dotnet: primary JetBrains IDE is Rider (standalone); IntelliJ does +# not have a first-party .NET plugin, so we emit nothing. +# - rust: JetBrains split Rust into standalone RustRover; the old +# intellij-rust plugin was discontinued. Emit nothing until Rust +# support is available as a first-party plugin again. +# - zig: community-maintained ZigBrains plugin. +stack_jetbrains_plugin() { + case $1 in + python) echo "PythonCore" ;; + scala) echo "org.intellij.scala" ;; + go) echo "org.jetbrains.plugins.go" ;; + ruby) echo "org.jetbrains.plugins.ruby" ;; + zig) echo "com.falsepattern.zigbrains" ;; + *) echo "" ;; + esac +} + # Returns space-separated dependency stack names (empty if none). stack_deps() { case $1 in diff --git a/cli/libexec/init/devcontainer b/cli/libexec/init/devcontainer index a09a0fb6..4dd307cf 100755 --- a/cli/libexec/init/devcontainer +++ b/cli/libexec/init/devcontainer @@ -124,6 +124,14 @@ devcontainer() { customize_devcontainer_json "$devcontainer_dir/devcontainer.json" "$project_name" "$ide" + # JetBrains plugins must be seeded after customize_devcontainer_json emits + # the customizations.jetbrains block. No-op for other IDEs. + if [[ "$ide" == "jetbrains" && -n "$stacks" ]]; then + local stacks_arr_jb=() + read -ra stacks_arr_jb <<< "$stacks" + customize_devcontainer_plugins "$devcontainer_dir/devcontainer.json" "${stacks_arr_jb[@]}" + fi + echo "Devcontainer dir created at ${devcontainer_dir#"$project_path"/}" | info } diff --git a/cli/test/init/jetbrains_plugins.bats b/cli/test/init/jetbrains_plugins.bats new file mode 100644 index 00000000..0a3f05bb --- /dev/null +++ b/cli/test/init/jetbrains_plugins.bats @@ -0,0 +1,71 @@ +#!/usr/bin/env bats +# shellcheck disable=SC2030,SC2031 + +setup() { + load test_helper + # shellcheck source=../../lib/devcontainer.bash + source "$SCT_LIBDIR/devcontainer.bash" + + DEVCONTAINER_JSON="$BATS_TEST_TMPDIR/devcontainer.json" + cp "$SCT_TEMPLATEDIR/devcontainer/devcontainer.json" "$DEVCONTAINER_JSON" + + # Emit the JetBrains customizations block so plugins tests have + # `"plugins": []` to rewrite. This mirrors the production order: + # customize_devcontainer_json runs before customize_devcontainer_plugins. + customize_devcontainer_json "$DEVCONTAINER_JSON" "my-project" "jetbrains" +} + +teardown() { + unstub_all +} + +@test "customize_devcontainer_plugins inserts a plugin for a stack that has one" { + customize_devcontainer_plugins "$DEVCONTAINER_JSON" "scala" + + run grep '"plugins": \["org.intellij.scala"\]' "$DEVCONTAINER_JSON" + assert_success +} + +@test "customize_devcontainer_plugins joins multiple plugin ids" { + customize_devcontainer_plugins "$DEVCONTAINER_JSON" "scala" "python" "go" + + run grep '"plugins": \["org.intellij.scala", "PythonCore", "org.jetbrains.plugins.go"\]' "$DEVCONTAINER_JSON" + assert_success +} + +@test "customize_devcontainer_plugins keeps empty array when no stack contributes a plugin" { + # java is bundled — stack_jetbrains_plugin returns empty for it. + customize_devcontainer_plugins "$DEVCONTAINER_JSON" "java" + + run grep '"plugins": \[\]' "$DEVCONTAINER_JSON" + assert_success +} + +@test "customize_devcontainer_plugins keeps empty array when called with no stacks" { + customize_devcontainer_plugins "$DEVCONTAINER_JSON" + + run grep '"plugins": \[\]' "$DEVCONTAINER_JSON" + assert_success +} + +@test "customize_devcontainer_plugins skips stacks whose plugin is bundled" { + # Mixing java (bundled → skipped) and scala (has plugin) should + # produce only the scala plugin. + customize_devcontainer_plugins "$DEVCONTAINER_JSON" "java" "scala" + + run grep '"plugins": \["org.intellij.scala"\]' "$DEVCONTAINER_JSON" + assert_success +} + +@test "customize_devcontainer_plugins does nothing when called on a vscode devcontainer.json" { + # Reset to a fresh vscode-style file (no jetbrains block, no `"plugins": []`). + cp "$SCT_TEMPLATEDIR/devcontainer/devcontainer.json" "$DEVCONTAINER_JSON" + customize_devcontainer_json "$DEVCONTAINER_JSON" "my-project" "vscode" + + customize_devcontainer_plugins "$DEVCONTAINER_JSON" "scala" + + # vscode block has "extensions": [...] but no "plugins": [] to rewrite. + # The function should have been a no-op. + run grep 'org.intellij.scala' "$DEVCONTAINER_JSON" + assert_failure +}