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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
gradle-build:
name: Gradle build (plugin + agent)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Build and test
run: ./gradlew build --stacktrace

agent-checks:
name: Agent end-to-end checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

# ubuntu-latest already ships Maven and Python 3, which the checks need
# (02-maven-plugin-realm.sh, dap-client.py); nothing extra to install.

# 02-maven-plugin-realm.sh runs `mvn -o` (offline) against these two poms, so their
# plugins must already be in ~/.m2/repository before that check runs.
- name: Cache local Maven repository
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('plugin/samples/maven/build-helper/pom.xml', 'plugin/samples/maven/enforcer/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-

- name: Warm the local Maven repo for the offline agent checks
run: |
mvn -q -f plugin/samples/maven/build-helper/pom.xml validate
mvn -q -f plugin/samples/maven/enforcer/pom.xml validate

- name: Run agent/checks
run: ./agent/checks/run-all.sh
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ is the in-plugin hook class used by the *source-rewriting* fallback
`fix:` tag.
- **No `Co-Authored-By` trailer.** The maintainer reviews and edits every commit before
pushing.
- **Commits not yet pushed to a branch's upstream are fair game to restructure.** Split,
merge, or reorder them freely — whatever makes the resulting history clearest thematically
and easiest to read. Once something is pushed, don't rewrite it without being asked.
132 changes: 108 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,59 @@
# bsh-plugin
# BeanShell Tooling — Debugging & Language Support for IDEs

BeanShell (`.bsh`) tooling: an IntelliJ Platform plugin for language support and an
in-editor debugger, plus a JVM debug agent that speaks the [Debug Adapter
Protocol][dap] so VS Code, Neovim and Eclipse can debug BeanShell too.
## IntelliJ-based IDEs plugin

One Gradle build, four subprojects:
JetBrains IDEA, WebStorm, CLion, and any other IntelliJ Platform IDE:

- BeanShell script recognition — `.bsh` files, a self-executing shebang, or an
`<!--language=BeanShell-->` hint comment
- **Full language support** — a hand-written parser, syntax highlighting, code completion,
navigation, refactoring, running scripts
- **Debugging** — breakpoints, stepping, a variables view, evaluate
- **Maven `pom.xml` injection** — the same language support and debugging for BeanShell embedded
in a Maven plugin's `<configuration>`

## VS Code extension

Debugging a `.bsh` script over DAP: attach to a JVM already running under the agent, or let the
extension launch it for you.

## Neovim plugin

Debugging support for `nvim-dap`, over the same DAP transport.

## Eclipse

Debugging support via LSP4E's generic DAP client (attach only — nothing here can launch the
target JVM itself).

## Description

This repository is two things, built together because the second depends on the first: a
source-level debugger for BeanShell, built once as a JVM agent and exposed twice — a native
protocol for the IntelliJ plugin above, and the Debug Adapter Protocol (DAP) for VS Code, Neovim
and Eclipse. See [`agent/`](agent/README.md) for the debugger and [`plugin/`](plugin/README.md)
for the language plugin.

## Specific documentation

**If you only want the IntelliJ IDE plugin**, [`plugin/README.md`](plugin/README.md) is the
complete reference — features, requirements, installation, screenshots.

**If you want to debug BeanShell from an editor other than IntelliJ**, jump straight to
[`editors/`](#editors-vs-code-neovim-eclipse) below.

## Why a debug agent, not JDWP

BeanShell scripts are not their own class files — they are interpreted by `bsh.Interpreter`
line by line, so the JVM's own debugger (JDWP) has nothing to attach *to* at the script
level; it can only see the interpreter's Java internals. The agent instead instruments
`bsh.Interpreter` itself (via ASM, at class-load time, `-javaagent`-style) so it can suspend
a script at a source line, report locals from the interpreter's own namespace, and evaluate
expressions with the real interpreter — without modifying the script on disk or the library
that embeds it. [`agent/README.md`](agent/README.md) has the full rationale and the
landmines that came with it.

## One Gradle build, four subprojects

```
plugin/ :plugin IntelliJ plugin -- language support and the debugger UI
Expand All @@ -16,20 +65,53 @@ editors/ -- VS Code extension, Neovim/Eclipse config
docs/ repository-wide docs
```

## The two pieces

**The IntelliJ plugin** ([`plugin/`](plugin/README.md)) adds BeanShell language
support to any IntelliJ-based IDE — syntax highlighting, a full AST parser, code
completion, navigation, running `.bsh` scripts, and Maven `pom.xml` injection.

**The debug agent** ([`agent/README.md`](agent/README.md)) is a JVM agent that
instruments `bsh.Interpreter` so BeanShell scripts can be debugged at the source
level, without modifying the script or the library that embeds it. The IntelliJ
plugin bundles it and talks to it over a native protocol; the same agent also
speaks DAP, so it works as a standalone debug adapter for editors that have their
own DAP client — see [`editors/vscode/`](editors/vscode/README.md),
[`editors/neovim/`](editors/neovim/README.md) and
[`editors/eclipse/`](editors/eclipse/README.md).
**The agent is a separate subproject on purpose**: once it speaks DAP it is a debug adapter
that VS Code, Neovim or Eclipse can attach to, and none of them will take it out of an
IntelliJ plugin ZIP. The IntelliJ plugin bundles the same agent jar and talks to it over a
native protocol by default (richer — per-thread suspension, multiple simultaneous stops —
than what IntelliJ's own DAP client would support); the two protocols are just two
serializations of the same instrumentation underneath.

## The IntelliJ plugin

[`plugin/`](plugin/README.md) adds BeanShell language support to any IntelliJ-based IDE
(IDEA, WebStorm, PyCharm, CLion, …):

- **Editing** — syntax highlighting, an AST-backed structure view, code folding, brace
matching, formatting, live/postfix templates, quick documentation.
- **Code intelligence** — a full recursive-descent parser matching the BeanShell grammar,
Go to Declaration / Find Usages / Rename, code completion, parameter hints, inspections
with quick fixes.
- **Java interoperability** (with the Java plugin present) — Ctrl+Click into Java classes
and members via static type propagation across a chain, and navigation into BeanShell
classes declared in a script.
- **Running** `.bsh` files with a bundled interpreter, and **Maven `pom.xml` injection** so
BeanShell embedded in Maven plugin configuration (`beanshell-maven-plugin`, the enforcer's
`evaluateBeanshell`, `build-helper-maven-plugin`, …) gets the same tooling.
- **Debugging** — line breakpoints, the call stack, Step Over/Into/Out, Run to Cursor, a
variables view with Watches and Evaluate, all backed by the real interpreter in the
selected frame. A companion Java (JDWP) session picks up breakpoints in Java code the
script calls into, for free, wherever the platform already wraps the JVM (e.g. debugging
an inline Maven script).

Full details, screenshots and known limitations: [`plugin/README.md`](plugin/README.md).

## Editors: VS Code, Neovim, Eclipse

The debug agent doubles as a standalone DAP debug adapter (`-Dbsh.debug.protocol=dap`), so
editors with their own DAP client can debug BeanShell without the IntelliJ plugin at all:

- [`editors/vscode/`](editors/vscode/README.md) — a VS Code extension with a `launch.json`
contribution and a `.bsh` language id. Supports both `attach` (to a JVM already running
under the agent) and `launch` (the extension starts that JVM itself).
- [`editors/neovim/`](editors/neovim/README.md) — configuration for `nvim-dap`, the same
transport.
- [`editors/eclipse/`](editors/eclipse/README.md) — configuration for Eclipse's generic DAP
client, attach-only (Eclipse's client has no scriptable way to launch the debuggee itself).

What DAP doesn't cover yet — conditional/function/exception breakpoints, step-back,
restart-frame — is listed honestly in the adapter's own capabilities rather than silently
ignored; see [`docs/PROTOCOL.md`](docs/PROTOCOL.md#9-relationship-to-dap).

## Building

Expand All @@ -41,16 +123,18 @@ JAVA_HOME=<jdk17+> ./gradlew :plugin:test
./gradlew :agent:instrument:shadowJar # the agent jar alone
```

The plugin needs **JDK 17+** (Gradle refuses less) and compiles Kotlin to 21. The
agent targets **Java 8**, because it loads into whatever JVM hosts BeanShell.
The plugin needs **JDK 17+** (Gradle refuses less) and compiles Kotlin to 21. The agent
targets **Java 8**, because it loads into whatever JVM hosts BeanShell — a per-task
`options.release`, not a build-wide property.

```bash
./agent/checks/run-all.sh # the agent, end to end
```

`agent/checks/` runs what a JVM test cannot arrange from inside itself: a real
`mvn` process, a JVM launched with `-javaagent`, and two processes talking over
the debug socket. See [`agent/checks/README.md`](agent/checks/README.md).
`agent/checks/` runs what a JVM test cannot arrange from inside itself: a real `mvn`
process (so a real plugin realm), a JVM launched with `-javaagent`, and two processes
talking over the debug socket. Run it after touching the agent or the wire protocol — see
[`agent/checks/README.md`](agent/checks/README.md) for what each check protects.

## Where to read first

Expand Down
10 changes: 5 additions & 5 deletions agent/checks/02-maven-plugin-realm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ MAVEN_OPTS="-javaagent:$AGENT_JAR -Dbsh.debug.trace=1" \
grep 'bsh-agent' "$CHECK_TMP/bh.txt" > "$CHECK_TMP/bh-agent.txt" || true

assert_contains "$CHECK_TMP/bh-agent.txt" 'src=inline evaluation of: ``prefix = project.getArtifactId();' \
"build-helper: the inline <source> is instrumented inside the plugin realm"
"build-helper: the inline <source> is instrumented inside the plugin realm" "$CHECK_TMP/bh.txt"
assert_contains "$CHECK_TMP/bh-agent.txt" 'line=1 src=inline evaluation of' \
"build-helper: lines are snippet-relative (first statement is line 1)"
"build-helper: lines are snippet-relative (first statement is line 1)" "$CHECK_TMP/bh.txt"
assert_contains "$CHECK_TMP/bh-agent.txt" 'line=3 src=inline evaluation of' \
"build-helper: the third statement reports line 3"
"build-helper: the third statement reports line 3" "$CHECK_TMP/bh.txt"

# --- the source-prefix filter -----------------------------------------------------------------
#
Expand All @@ -60,7 +60,7 @@ MAVEN_OPTS="-javaagent:$AGENT_JAR -Dbsh.debug.trace=1 -Dbsh.debug.sources.file=$
grep 'bsh-agent' "$CHECK_TMP/filtered.txt" > "$CHECK_TMP/filtered-agent.txt" || true

assert_contains "$CHECK_TMP/filtered-agent.txt" 'src=inline evaluation of' \
"filter: a prefix computed by the production rule still matches the script"
"filter: a prefix computed by the production rule still matches the script" "$CHECK_TMP/filtered.txt"
assert_not_contains "$CHECK_TMP/filtered-agent.txt" 'print.bsh' \
"filter: BeanShell's own print.bsh is excluded"

Expand All @@ -83,7 +83,7 @@ if [[ -f "$ENFORCER" ]]; then
mvn -o -q -f "$ENFORCER" validate > "$CHECK_TMP/enf.txt" 2>&1
grep 'bsh-agent' "$CHECK_TMP/enf.txt" > "$CHECK_TMP/enf-agent.txt" || true
assert_contains "$CHECK_TMP/enf-agent.txt" 'src=inline evaluation of' \
"enforcer: the inline <condition> is instrumented too"
"enforcer: the inline <condition> is instrumented too" "$CHECK_TMP/enf.txt"
else
printf ' \033[33mNOTE\033[0m no enforcer sample at %s, skipping that half\n' "$ENFORCER"
fi
Expand Down
16 changes: 14 additions & 2 deletions agent/checks/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,31 @@ fail() {
fi
}

# assert_contains <haystack-file> <needle> <description>
# Prints the tail of a file indented to line up under a fail() message.
_tail_context() {
printf ' --- last %d lines of %s ---\n' "$2" "$1"
tail -n "$2" "$1" | sed 's/^/ /'
}

# assert_contains <haystack-file> <needle> <description> [<raw-output-file>]
#
# The optional 4th argument is the *unfiltered* command output the haystack was extracted from
# (e.g. by grep). On failure it's dumped to the log: the haystack alone is often just the empty
# result of that extraction, which says nothing about why -- the raw output usually does.
assert_contains() {
if grep -qF -- "$2" "$1"; then
pass "$3"
else
fail "$3" "not found: $2"
[[ -n "${4:-}" && -f "$4" ]] && _tail_context "$4" 20
fi
}

# assert_not_contains <haystack-file> <needle> <description>
# assert_not_contains <haystack-file> <needle> <description> [<raw-output-file>]
assert_not_contains() {
if grep -qF -- "$2" "$1"; then
fail "$3" "unexpectedly found: $2"
[[ -n "${4:-}" && -f "$4" ]] && _tail_context "$4" 20
else
pass "$3"
fi
Expand Down
Loading
Loading