Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions cli/lib/devcontainer.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions cli/lib/stacks.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions cli/libexec/init/devcontainer
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
71 changes: 71 additions & 0 deletions cli/test/init/jetbrains_plugins.bats
Original file line number Diff line number Diff line change
@@ -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
}
Loading