Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>DC-Grails-VSNew to Visual Studio Code? Get it now.
DC-Grails-VS

DC-Grails-VS

Lei Gao

|
27 installs
| (0) | Free
Grails development for VS Code: breakpoints that actually bind in .groovy files, closures included, plus the Grails and Gradle commands.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

DC-Grails-VS

Pre-release. The debugger works: breakpoints bind in .groovy files, closures included, and scripted sessions have been driven end to end against Grails 7.2.3 and 8.0.0. The main flows have also been seen working in a real VS Code window (see Requirements for what has not). But it has been exercised on two applications, by one person. Expect to find things. Known gaps are listed at the bottom; the issue tracker is on GitHub.

Marketplace gaolei8888.grails-gradle-extension
Source github.com/gaolei8888/dc-grails-vs
Issues github.com/gaolei8888/dc-grails-vs/issues
Changelog CHANGELOG.md

Version 0.1.31 is a pre-release. To install it, open the Marketplace page above and choose Switch to Pre-Release Version, or from the command line (the --pre-release is needed while this is one):

code --install-extension gaolei8888.grails-gradle-extension --pre-release

Grails and Gradle commands from the command palette and a sidebar tree — and a debugger that can actually stop on a line of Groovy.

Breakpoints in .groovy files

VSCode does not bind breakpoints in Groovy. The Java debugger resolves "file → class name" through JDT, and JDT does not index .groovy, so the breakpoint is never registered with the JVM and the dot stays hollow. No setting changes this.

This extension ships its own debug adapter. In attach mode it does not parse Groovy at all: it asks the running JVM which class owns a line, which is the only authoritative answer there is. Closures come out for free — a synthetic closure class reports the same source file as the class that encloses it — and classes that are not loaded yet are picked up when they load.

What that means in practice:

  • Breakpoints bind in controllers, services, domain classes and inside closures, including nested ones.
  • They bind in methods rewritten by Grails' AST transforms — a @Transactional method whose body has been moved into $tt__… still stops on the right line.
  • A breakpoint on a line with no bytecode (a method signature, a blank line) slides down to the first line that can hold one, and says so.
  • Variables captured by a closure are shown as their values, not as the groovy.lang.Reference the compiler boxed them in.
  • A Grails scope beside Locals whenever the thread is serving a request, with params, request, response and session. None of those is a local or a field of anything on the stack — they belong to the request, which Spring keeps in a thread local — so nothing else shows them.
  • Maps show their entries. params reads as controller, action, id, not as a wrapper around a table and a modCount.
  • A domain object shows its own properties. GORM's traits compile a field into every domain class they touch, and a class with one property of its own arrives carrying half a dozen of them; those are hidden, and id and version are not.
  • Step over lands on the next line of your code, including on the way back out of a call. It is built out of breakpoints rather than JDI stepping, which in a Grails application would sometimes run to the end of the method instead.
  • Step into enters a @Transactional method, and enters a closure: stepping into (1..3).each { i -> lands in the closure body. Step out walks back through the wrapper frames Grails generates, which have no lines to stop on.
  • Stepping skips the framework: the Groovy runtime, reflection, the transaction template and the servlet container are stepped over rather than into.
  • Exception breakpoints, caught and uncaught, filtered the same way: it stops where your code threw, not in the framework. A Grails request throws and catches its way through startup and dispatch, and stopping for all of it would be unusable.

Exception breakpoints are in the Breakpoints section of the Run and Debug view. The exception's message is read from the object rather than by calling getMessage(), so nothing runs in the application to describe a stop.

Run Grails: Debug App. It starts gradlew bootRun --debug-jvm, waits for the JVM to report that it is listening, and attaches.

For an application this extension did not start — a war on a server, one started from a terminal, one in a container with the agent on — Grails: Attach to Running App asks for a host and port and attaches to it. Or write your own attach configuration:

{
  "type": "groovy-dap",
  "request": "attach",
  "name": "Attach to Grails (Groovy)",
  "hostName": "localhost",
  "port": 5005
}

The type is groovy-dap, not groovy: another Groovy extension may already own groovy, and VS Code allows one debug adapter per type. A launch.json written for 0.1.29 or earlier with "type": "groovy" keeps working as a compatibility alias, as long as no other extension owns groovy (the "Grails - Debug" output channel says so when one does); change it to groovy-dap when convenient, and you must on a machine with Code Groovy. If you have Code Groovy installed, its own "Groovy / Grails..." entry in the Run and Debug dropdown is Code Groovy's, not this extension's.

Launch configuration

Starting the debugger from Run and Debug runs ./gradlew bootRun --debug-jvm and attaches; F5 with no launch.json in a Grails workspace is meant to do the same. It attaches instead of starting a second app if one is already listening, and ending the session stops an app it started. Launch attributes: projectDir, args, env, startTimeout, sourcePaths. The debug port is not configurable, because Gradle hard-codes 5005.

Checked in a real VS Code window (2026-10-12, Grails 8.0.0, JDK 21): the Run and Debug button opens "Select debugger"; picking Grails (Groovy debugger) (the debugger's label) starts the app and attaches within about 10 seconds; a request stops at the breakpoint with the right values; Continue resumes; Stop in the status bar frees the ports. On later runs the button starts it directly. Starting it again while a debugger is attached shows a warning and does not start a second app. The name Grails: Debug App is the configuration's name: it appears in a generated launch.json, under Add Configuration, and in the "Select Launch Configuration" list (one entry, for the workspace folder; builds of 0.1.7 before the fix also listed a duplicate under "user settings"). Not yet verified: F5, and the F10/F11 keys.

Breakpoints in .gsp files

GSP files include syntax highlighting for HTML, namespaced Grails tags (including custom tag libraries), ${…} expressions, directives, scriptlets and GSP comments. JavaScript and CSS inside script/style blocks keep their highlighting. Toggle Block Comment uses %{-- … --}%.

Set one in the gutter of a GSP and it binds, stops on the page's own line, and shows the .gsp in the call stack. The Grails scope, the variables pane and conditions all work there.

There is no SMAP behind this and no JSR-45: a GSP compiles into a class named after the page's path, reporting no source file and no second stratum. The only mapping that exists while the application runs is an array of line numbers held by Grails, read as a field — so nothing runs in your application to place a breakpoint.

A page is compiled the first time it renders, so a breakpoint set before that stays hollow and says so; render the page once and it binds. A line that generates no code — a closing tag, blank markup — says that instead.

One line of markup becomes several statements in several classes, and the breakpoint goes on the first of them, so a line stops once per pass rather than two or three times. A line inside a <g:each> stops once per iteration.

Stepping works inside a page: step over moves a line of markup at a time, and step into enters a tag body.

Grails 7 compiles pages at runtime, in a packaged war as much as in a development run, so there is no precompiled case to handle. A page compiled from inside a war is a differently named class, because the name comes from the path; the part below views is the same either way and that is what a breakpoint matches on, so debugging a deployed application works from your copy of the file.

Debugging tests

Grails: Debug Tests runs gradlew test --debug-jvm and attaches to the test JVM, which is the same machinery the application uses — Gradle forks it with the same agent on the same port and announces it on the same stream. It offers the spec in the active editor as the filter, so debugging the failure you are looking at is one command; leave the box empty to debug the whole run.

Commands

Every Grails command and Gradle task this project knows about is registered — 205 of them. The ones people reach for are in the Grails view in the activity bar, under Run, Create (empty), Scaffold from domain class, Database Migration and Gradle; the rest are in the command palette under Grails: and Gradle:.

Create and Scaffold are not the same thing. create-controller writes an empty controller; generate-controller scaffolds one, actions and all, from a domain class that already exists — which is why every entry under Scaffold asks for one.

Run, Debug and Stop are also in the status bar (shown only in a Grails workspace: a top-level grails-app folder, or a gradle.properties / build.gradle / build.gradle.kts that mentions "grails"; a Grails app in a subfolder is not detected). While a build is running the pair is replaced by a spinner that turns into a check and the address once the application reports that it is serving; clicking it opens the app, or shows the build output while it is still starting.

The application's log goes to an output channel — Grails - Normal for Run App, Grails - Debug for Debug App — reachable any time with Grails: Show Output.

Requirements

  • VS Code 1.90 or newer.
  • A Grails project with a Gradle wrapper (gradlew) in the folder you open.
  • Grails 7.x or 8.x. Checked against 7.2.3 and 8.0.0 by scripted debug sessions. In a real VS Code window (2026-10-11) the status bar buttons, Debug App, breakpoint hits, Locals and Watch values, evaluating a variable path in Watch, changing a value, and breakpoint rebinding after a class reload were seen working. The launch configuration, stepping with the toolbar buttons and the JDK search were seen working on 2026-10-12 (see Launch configuration). Not yet seen in a real window: F5, the F10/F11 keys, hover, and Windows and Linux paths. A Grails 8 application itself needs Java 21 or newer.
  • A full JDK, 17 or newer (not a JRE: the adapter uses the jdk.jdi module). The JDK your application runs on can be a different one.
  • JDK discovery. The debug adapter's JDK is searched in this order, and the first usable one wins: grails.debug.javaPath, JAVA_HOME from grails.run.env or the environment, macOS java_home, SDKMAN, the usual install folders (Homebrew, Linux, Windows), then java on PATH (the macOS /usr/bin/java stub is skipped). A usable JDK is Java 17 or newer with the jdk.jdi module. For the application's build (gradlew) the only check is that JAVA_HOME's bin/java is executable: a valid JAVA_HOME of any version is left alone, and the search only fills one in when there is none. The single error naming the ways out appears only when there is no usable java at all (none found and none on PATH, or only the macOS stub). A java that is on PATH but too old or without jdk.jdi is still handed to the adapter, which then fails to start with Java's own message; point grails.debug.javaPath at a full JDK 17+. VS Code usually resolves your shell environment itself, so JAVA_HOME is normally there; this search is a fallback for when it is not (seen once, cause unknown). It was verified with VS Code started with an empty environment (no JAVA_HOME, a bare PATH): the app started and attached using the SDKMAN JDK. You can also set grails.run.env ({"JAVA_HOME": "..."}) or grails.debug.javaPath, or start VS Code from a terminal with code ..
  • For grails.debug.adapter: "java" only: the Debugger for Java extension.

Settings

Setting Default What it does
grails.run.command "" Command line to start the app instead of the Gradle wrapper. For projects that boot through a script which exports the environment the app needs.
grails.run.debugCommand "" Same, for Debug App. Defaults to grails.run.command with --debug-jvm appended.
grails.run.env {} Environment variables to add for the app process.
grails.run.args [] Extra arguments for bootRun.
grails.debug.adapter groovy groovy uses the adapter in this extension. java hands the session to vscode-java-debug, which cannot bind a breakpoint in a .groovy file.
grails.debug.javaPath "" The java used to run the debug adapter. Defaults to the discovery order above.

Attach configurations also take sourcePaths (roots for resolving stack frames, defaulted from the project layout), stepFilters (packages to step over), stepIntoProjectCodeOnly and trace.

Stopping

Stop reaches the application, not just the build. Gradle forks the app from its daemon, and the daemon is reused between builds, so the app is only sometimes a descendant of the process this extension started — killing that process tree leaves the app running about as often as not, holding the debug port and 8080. Stop kills the tree and, separately, whatever JVM is listening on the ports the app announced.

Debug App also checks the debug port before starting. If something already holds it, it says what, and offers to stop it — because a JVM that cannot open its debug port produces a build that stops after :findMainClass and prints nothing at all, which is indistinguishable from a hang.

Hover and watch

Hovering a name shows its value, and watch expressions work, for anything that is a path: user.name, params['id'], items[0].label, this.someService. Names from the Grails scope resolve too, so params and request work in a hover even though neither appears in the source.

Read, not run. A method call, an operator or a closure needs an expression compiled and executed inside the application, which means resuming its threads to do it -- so those are refused, by name and with the reason, rather than reported as missing.

Logpoints and hit counts

Right-click in the gutter for Add Logpoint: a breakpoint that prints and carries on rather than stopping. Anything in braces is read the way a hover is, so

iteration {i}, so far {doubled}, from {params.id}

prints iteration 2, so far [105, 106], from 47. Lists and maps are written out rather than shown as an object id — up to eight entries, two levels deep. An expression it cannot read prints the reason in place of the value instead of disappearing.

Hit Count takes a number, optionally after an operator:

100 stop from the hundredth hit onward (same as >=100)
=100 stop on the hundredth hit only
>100 <100 <=100 as they read
%10 stop on every tenth hit

The count is of hits that would have stopped, so the second copy Groovy compiles of every line does not advance it. A hit count that cannot be read is reported on the breakpoint rather than ignored.

Conditions

The Condition box takes a comparison:

params.id == '5'      i > 3      user.name != null      w.count <= limit

Each side is a path or a literal — the same reading a hover does — so names from the Grails scope work here as well: params.action == 'index' is a condition although params is not a local of anything on the stack.

== compares by value, the way Groovy's does. Anything that needs an expression compiled and run inside the application — a call, arithmetic, a closure — is reported on the breakpoint, and that breakpoint then stops every time. A condition quietly treated as false would be a breakpoint that never fires, which reads as a broken debugger rather than as a condition to rewrite.

With a hit count as well, the count advances only when the condition was true: i > 1 with =2 stops the second time i > 1 holds.

Data breakpoints

Right-click a field in the variables pane and choose Break on Value Change. The debugger stops where the write happens and says what the value was and what it is about to become:

dapspike.SpikeService.touches: 0 -> 1

Watching a field is a JVM facility, so nothing runs in your application to arm one. It watches the object you pointed at rather than every instance of the class, which is what "this variable" means in the pane. Reads can be watched too where the JVM allows it.

Only fields can be watched — that is all the JVM reports writes to — so the option does not appear for a local, an array element or a map entry, and a final field is refused because it is never written again.

Changing a value

A local, a field, an array element or an existing map entry can be changed from the variables pane while the target is stopped: numbers, strings, booleans and null. Setting seed from 4 to 7 at the top of a service method changes what the application then returns.

A local that a closure captured is written inside the groovy.lang.Reference it is boxed in, not over it — replacing the box would leave the closure holding the old one. A map entry is changed in place; a key that is not there cannot be added this way.

This is the one place the debugger runs code in your application. JDI will not put an int into a slot typed java.lang.Integer, and Groovy types nearly everything that way, so making a boxed number calls valueOf on the box class — a JDK static that takes no locks — single-threaded. Reading a value never runs anything.

Explain an exception

Requires VS Code 1.90 or later. When paused on a Groovy exception, click Explain Exception in the status bar or run Grails: Explain Exception. Choose one of the language models available in VS Code. The explanation streams into a read-only document beside your code and includes the context used.

The request sends the exception type/message, up to twelve application frames, and nearby source from up to three workspace files. It does not collect locals, Grails request/session values or environment variables. Source and exception messages can themselves contain sensitive text; Grails: Show Exception Context lets you inspect the local snapshot before choosing to use a model. No data is sent automatically when a breakpoint is hit. The snapshot uses current editor text (including unsaved changes), which may differ from a deployed application.

Model access and any initial consent are handled by VS Code's Language Model API. No particular model or vendor is required. If no model is available, the command opens the local exception context with setup guidance. Cancel the progress notification to stop a request; continuing or stepping the debugger also cancels it. Explanations suggest changes but do not edit files or execute application code.

Development and tests

Run pnpm install, npm run build:server, pnpm test, and pnpm lint. The debug adapter has its own dependency-free checks: cd server && ./gradlew verifyProtocol (also run by ./gradlew build). pnpm test:editor runs the real VS Code/Grails integration suite in an isolated profile. See testing instructions for the standalone testbed, environment overrides and test coverage.

Known issues

  • Step over once skipped code. It was reported that after stepping over the last line of a service method back into the controller, a further step over skipped the remaining code. It could not be reproduced afterwards (several target shapes, JDK 21 and 25, Grails 7 and 8), and the cause is unknown.
  • Step into a @Transactional method enters it (scripted runs on Grails 7.2.3 and 8.0.0, 2026-10-12). Inside a .each closure, Step Into stops in the closure for the first iteration only and then continues past the loop; put a breakpoint in the closure to stop on every iteration. Step Into relies on sourcePaths being the project's source roots (the extension fills these in): with the application folder itself as the only root, Step Into does not enter calls.
  • Watch, hover, the Debug Console, logpoints and conditions read variable paths only (a.b.c, list[0], map['k']); nothing is run in the application, and list.size() is refused by name, with the reason. General expressions are not implemented.
  • Simple variable values can be changed in the Variables panel: numbers, true / false, characters, strings and null, written as literals. A number put into a boxed or Object slot is made by running valueOf of one of the JDK box classes in the application (the only code the debugger runs there). Not supported: objects and collections. Editing from Watch (setExpression) is deferred. Changing a value was seen working in a real VS Code window with the earlier adapter; which request VS Code sends for it is not known.
  • Hot reload needs a recompile. Saving a source file alone does not change the running app. Once the class was reloaded (what triggers that was not established), breakpoints rebound to the new class and showed the new value; seen in one scenario in a real window.
  • Step Into with nothing to enter does nothing. On the last line of an action it stays on that line; Step Out or Step Over there lets the request finish without stopping in framework code.
  • Stop while paused: if the JVM is stopped at a breakpoint, Stop only ends the Gradle client; the application and the adapter stay until you Continue.
  • Stopping kills any java process listening on the debug port (5005), which Gradle hard-codes.
  • Code Groovy's own classpath script may print Cannot mutate the state of configuration ':compileOnly' in its output channel on Grails projects; it is harmless and not from this extension.
  • Two different applications serving the same view path in one JVM would make a GSP breakpoint ambiguous; it binds to neither rather than guessing.
  • The AI command's real exception capture and no-model behavior have been tested in VS Code. Model streaming/error/cancellation paths have automated tests with a simulated provider; a live provider response still needs verification.

Star it

If you use this, clone it, or build something of your own on top of it, please star the project on GitHub. It takes one click, and it is the main way anyone else finds out this exists.

License

MIT

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft