Skip to content
| Marketplace
Sign in
Visual Studio Code>Other>Java Save Actions PlusNew to Visual Studio Code? Get it now.
Java Save Actions Plus

Java Save Actions Plus

Umang Purwar

|
1 install
| (0) | Free
Custom Java save-action cleanup pipeline for VS Code. Adds save-time cleanup rules on top of standard Java formatting and import organization workflows.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Java Save Actions Plus


1. Summary

  • What it is: a custom VS Code extension that adds extra Java save-time cleanup, on top of what Red Hat's Java extension already does natively — aimed at closing the gap with STS/Eclipse's save-action behavior.
  • Why it exists: STS (Spring Tool Suite) gives developers dozens of automatic save-time cleanups via Eclipse's JDT framework. VS Code doesn't ship an equivalent out of the box. This project closes part of that gap.
  • What works today: native Red Hat Java cleanup (configured via settings) plus 4 custom text-level save rules running on every Ctrl+S.
  • What doesn't work yet: full STS parity. The harder cleanups need real type/AST information our text rules don't have. Also: installing this into normal managed company VS Code is blocked by policy — it runs via Extension Development Host (F5) or portable VS Code instead.
  • Bottom line: the POC proves the mechanism works and is fast enough for real use. It is not, and does not claim to be, a full Eclipse replacement.

2. Problem Statement

  • Our team works in both STS and VS Code.
  • STS includes Eclipse's full save-action framework — on every Ctrl+S, Eclipse can run dozens of configurable cleanup rules backed by the real JDT AST (type-aware, not just text pattern matching).
  • VS Code does not ship an equivalent mechanism by default.
  • The Red Hat Java extension (redhat-developer/vscode-java) brings real JDT language services into VS Code, including a documented subset of cleanup actions — 20 confirmed actions, verified directly against the extension's own document/_java.learnMoreAboutCleanUps.md file. That's real coverage, but it doesn't match Eclipse's full catalog, and it doesn't run on save unless explicitly configured.
  • This gap creates friction for developers switching between STS and VS Code. This project closes part of that gap with a custom extension layered on top of the native coverage.

3. What I Tried

3.1 Native Red Hat cleanup route

Configured java.cleanup.actions + editor.codeActionsOnSave to run Red Hat's built-in cleanups automatically on save. Result: real value, 20 confirmed actions, but not full STS parity — many Eclipse cleanup types aren't exposed through this list at all.

3.2 Formatter XML route

Pointed VS Code's Java formatter at an Eclipse-exported formatter XML profile (same one STS can use), via java.format.settings.url. Result: this controls code style (indentation, line breaks, spacing) — it's a separate subsystem from cleanup actions in both Eclipse and VS Code. Useful for formatting parity, but doesn't touch the cleanup gap at all (unused imports, annotations, lambda conversion, etc. are handled elsewhere).

3.3 OpenRewrite / external-process route

Tried triggering OpenRewrite cleanup recipes on save via a PowerShell watcher and a Gradle/Maven plugin hook. Result: technically interesting, explicitly rejected for save-time use:

  • Too slow — spawning a Gradle/Maven process or JVM on every Ctrl+S adds multi-second latency.
  • Too fragile — project-root detection, wrapper invocation, and merging results back into the editor needed too much glue.
  • Not ruled out forever, just explicitly out of scope for phase-1.

3.4 v1 custom extension:

Built a working extension from scratch. Hooked into onDidSaveTextDocument, read file text, applied custom rules, wrote cleaned text back via WorkspaceEdit. Proved the entire mechanism works, with 4 working rules:

  1. Remove trailing whitespace
  2. Remove obvious literal casts ((String) "abc", (int) 5, (long) 10L)
  3. Wrap single-line control bodies in braces (if, for, while)
  4. Diamond operator simplification (new ArrayList<String>() → new ArrayList<>())

These are text-level rules, not JDT parity — but they work reliably.

3.5 v2 : build using the documented code from eclipse and red hat

v1 was a working but unstructured proof of concept — pipeline logic, rules, and config mixed together in a way that's hard to extend safely. v2 restructures the same proven mechanism into clean modules (native docs, pipeline, rules, text rules, research/gap-analysis) so the team can actually maintain and extend it. v2 is not a rewrite of history — it's the same proven approach, organized properly.

3.6 failed workaround that i tried

Installing a custom .vsix into managed company VS Code is blocked by extension allowlist policy. Two confirmed workarounds:

  • Portable VS Code: a separate downloaded VS Code instance, configured independently — no policy interaction, works for install + demo.
  • Extension Development Host (F5): the standard developer test workflow — launches a real VS Code window with the extension pre-loaded, no install step needed at all.

F5 is the recommended path for day-to-day dev and demos — no separate install, fastest iteration loop.


4. Current State Today

  • v1 = the working fallback POC. Proves the save pipeline + 4 text rules work end to end. Do not delete it. If v2 ever breaks, v1 still works.
  • v2 = the cleaner, structured extension — this is what the rest of this doc covers, and what should be used for ongoing work and demos.
  • Currently working in v2: extension activation, save listener, all 4 custom rules (trailing whitespace, literal casts, diamond operator, control statement blocks), native Red Hat cleanup running alongside, and save-loop protection.
  • Not yet implemented in v2: any cleanup beyond the 4 proven rules; any AST-backed rule; any rule requiring real type information.

5. How to Set Up and Run v2 on Another Laptop

Prerequisites

  • VS Code installed
  • Node.js 18+ and npm
  • The kb-java-save-actions-v2 repo checked out locally
  • Red Hat's Extension Pack for Java installed in VS Code (native cleanup + Java language support)

Steps

  1. Open the kb-java-save-actions-v2 folder in VS Code.
  2. Run:
    npm install
    npm run compile
    
  3. Press F5.
  4. A new window opens — the Extension Development Host. It's a real VS Code window with the extension active inside it — treat it as a normal editor, not a sandbox.
  5. In that new window: open a real Java project (File → Open Folder).
  6. Open a .java file, make a small messy edit, press Ctrl+S.
  7. Check the output: View → Output, pick Java Save Actions Plus from the dropdown.

Enable native cleanup (one-time, per project or globally)

{
  "java.cleanup.actions": [
    "organizeImports",
    "addOverride",
    "addFinalModifier",
    "qualifyMembers",
    "qualifyStaticMembers",
    "lambdaExpressionFromAnonymousClass",
    "lambdaExpression",
    "redundantModifiers",
    "redundantSuperCall"
  ],
  "editor.codeActionsOnSave": {
    "source.cleanup.java": "explicit"
  }
}

Adjust to taste — some actions (like qualifyMembers) are style preferences, not universal.

Package as VSIX (for portable VS Code demo, not managed VS Code)

npm install -g @vscode/vsce
vsce package
code --install-extension java-save-actions-plus-1.2.0.vsix

Install into a portable VS Code instance:

code --install-extension java-save-actions-plus-1.2.0.vsix

Common limitations

Gotcha Fix
Testing in the wrong window Edit Java files in the Extension Development Host window, not your original VS Code window
Output panel shows nothing Dropdown defaults to Tasks — switch it to Java Save Actions Plus
Save seems to "hang" A breakpoint is probably still set in the source and execution is paused — check the original (debugger) window
Compile errors after editing a rule Usually a mismatched import/export name — check the rule file's export matches what rulesRegistry.ts imports
Can't install in normal VS Code Expected — managed company VS Code blocks custom VSIX installs by policy. Use F5 or portable VS Code instead.

6. How v2 Works Internally

On Java file save, in order:

  1. File is saved (Ctrl+S, or a save triggered by another tool).
  2. The save listener in pipeline/savePipeline.ts receives the event.
  3. It checks: is this a real user save, or one our own extension just triggered? (Save-loop guard — Section 9.) If it's ours, skip.
  4. Config is checked (src/config.ts) — is the pipeline enabled? Are custom rules enabled?
  5. Active rules are pulled from rules/rulesRegistry.ts.
  6. rules/ruleRunner.ts runs each active rule, in order, over the file's text.
  7. If anything changed, pipeline/documentRefresh.ts applies the new content back to the editor and saves it.
  8. That save is marked internally so it doesn't re-trigger step 2 (the loop guard).
  9. Everything logs to the Java Save Actions Plus output channel.

File / folder reference

File / folder Purpose When you touch it
src/extension.ts Entry point, activation, command registration Rarely — new commands or activation changes
src/config.ts Reads extension settings Adding a new setting
src/logger.ts Output channel logging Rarely
pipeline/savePipeline.ts Save listener, debounce, save-loop guard, orchestration Changing save behavior or fixing pipeline bugs
pipeline/documentRefresh.ts Applies cleaned content back to file/editor; includes the active-editor-mismatch guard (modeled on vscode-java issue #557) Changing how edits are applied
rules/rulesTypes.ts The KbRule interface (id, label, apply()) every rule must implement Only if the rule contract itself changes
rules/ruleRunner.ts Runs the active rules in order, catches errors per-rule Rarely
rules/rulesRegistry.ts Which rules are active Every time you add or remove a rule
text/*.ts The actual custom save-action rules Adding/fixing a rule
native/nativeCleanupMap.ts Documents which actions Red Hat already covers natively Confirming/updating native coverage
research/stsActionMap.ts Full Eclipse/JDT cleanup taxonomy with coverage tier (native / kb-text / gap / uncertain) Mapping a new STS action to a strategy
package.json Extension manifest, commands, settings, scripts Adding commands/settings, version bump
tsconfig.json TypeScript build config Rarely
README.md Repo-level overview Keep in sync with Section 7 below

7. Native Red Hat Coverage vs KB Custom Rules

7.1 Native Red Hat cleanup actions

Verified directly against redhat-developer/vscode-java's own document/_java.learnMoreAboutCleanUps.md — 20 confirmed action IDs, no more, no less. Do not re-implement any of these.

Native ID What it does
qualifyMembers Adds this. to unqualified field/method access
qualifyStaticMembers Qualifies static access with the declaring class name
addOverride Adds missing @Override (including interface implementations)
addDeprecated Adds missing @Deprecated when Javadoc has @deprecated
stringConcatToTextBlock Converts qualifying String concatenations to text blocks (Java 15+)
invertEquals Inverts .equals()/.equalsIgnoreCase() calls to avoid NPE risk
addFinalModifier Adds final to variables/params/fields where legal
instanceofPatternMatch Converts instanceof + cast to pattern matching (Java 15+)
lambdaExpressionFromAnonymousClass Converts anonymous functional-interface class to lambda (Java 8+)
switchExpression Converts switch statements to switch expressions (Java 14+)
tryWithResource Converts try/finally-close to try-with-resources
lambdaExpression Simplifies lambda syntax (removes unneeded parens, converts to method ref, etc.)
organizeImports Organizes/removes unused imports
renameUnusedLocalVariables Renames unused loop/lambda/pattern variables to _
useSwitchForInstanceofPattern Converts instanceof if/else chains to pattern-matching switch
redundantComparisonStatement Removes redundant comparison statements
redundantFallingThroughBlockEnd Removes redundant end-of-block jump statements
redundantIfCondition Simplifies redundant if/else-if conditions
redundantModifiers Removes redundant modifiers (e.g. public on interface members)
redundantSuperCall Removes a no-op super() call in a constructor

Configure via:

{
  "java.cleanup.actions": ["organizeImports", "addOverride", "..."],
  "editor.codeActionsOnSave": { "source.cleanup.java": "explicit" }
}

7.2 Custom v2 rules (phase-1, text-level)

Rule ID File What it does Scope
kb.trailingWhitespace text/trailingWhitespaceRule.ts Removes trailing whitespace per line All lines
kb.unnecessaryLiteralCast text/unnecessaryLiteralCastRule.ts Removes obvious redundant casts on literals, e.g. (String) "abc" Literal-only — does not attempt general cast removal
kb.controlStatementBlock text/controlStatementBlockRule.ts Wraps a bare if (...)/for (...)/while (...) followed by a single next-line statement into a { } block Common single-line case only
kb.diamondOperator text/diamondOperatorRule.ts new ArrayList<String>() → new ArrayList<>() No-arg constructors with an explicit generic type only

These are text-level pattern-matching rules, not real Java parsing. They don't understand types, scope, or semantics — only text shape. They're deliberately conservative: when a case looks ambiguous, the rule does nothing rather than risk an incorrect edit.

7.3 Known coverage gaps

These are confirmed to exist in Eclipse's cleanup framework but are not yet implemented in v2:

Gap Why it's not in phase-1
StringBuilder/StringBuffer conversion Correct behavior needs type/loop context — not text-safe (Eclipse reference: StringFixCore.java)
Enhanced-for-loop conversion Requires iterable type checking — not text-safe
Lazy logical operator substitution Type-sensitive — needs real AST
Static inner class detection Needs scope/capture analysis — high difficulty
Primitive cleanup family (comparison/parsing/wrapper-vs-primitive) All type-resolution-dependent — do not attempt as text rules
Boolean literal simplification (full Boolean.TRUE/Boolean.FALSE case) The narrow == true/== false case is text-safe; the full wrapper-type case needs type info
Unnecessary cast removal (general, beyond literals) Needs real type resolution (Eclipse reference: UnusedCodeFixCore.java) — only the literal-only case is text-safe

8. Design Principles in v2

v1 proved the mechanism works but mixed pipeline logic, rules, and config into a small number of files — workable for a proof of concept, hard to extend safely as a team. v2 keeps the exact same proven approach, restructured around these principles:

  1. Don't rebuild what Red Hat already gives us. If an action is in java.cleanup.actions, configure it — don't custom-code it.
  2. Text rules are honest about scope. Every rule has a documented covered/not-covered boundary. No rule claims more than its text pattern can provably guarantee.
  3. Eclipse JDT is the behavioral reference. For every rule, the corresponding Eclipse fix class is the source of truth for intended behavior.
  4. Uncertain things are marked uncertain, not silently assumed correct.
  5. Phase-1 is fast and in-process. No external JVM, no Gradle, no OpenRewrite on every save.
  6. v1 is the fallback. Don't delete it.

9. Current Limitations / Known Gaps

  • KB custom rules are text-level, not AST-based — no understanding of types, scope, or semantics.
  • Not every STS/Eclipse save action is implemented — only the safe, provable wins so far (Section 7.3 lists the confirmed gaps).
  • Native coverage and KB custom coverage live in two separate systems — there's no single switch showing "everything that happens on save." Check both Section 7.1 and your settings.json.
  • Managed company VS Code blocks normal .vsix install. This is a policy constraint, not a bug — F5 / Extension Development Host or portable VS Code are the working paths.
  • Save-loop bug (fixed): the extension used to re-trigger itself endlessly after applying its own edit. Fixed with an internal-save guard in pipeline/savePipeline.ts — when the extension saves a file itself, it marks that save so the resulting onDidSaveTextDocument event is recognized as "ours" and skipped exactly once, instead of re-entering the pipeline.
  • Real STS/Eclipse parity on the harder rules would need an AST-based approach — most realistically a small Java helper process using actual JDT parsing, not more text rules. Not built yet.

10. How to Debug It

  1. Run via F5 (Section 5).
  2. Watch the Java Save Actions Plus output channel — every step logs there.
  3. To debug into code: set a breakpoint in any .ts file, trigger a save in the Extension Development Host window — execution pauses in your original (debugger) window.
  4. Don't leave breakpoints set for a demo — every save will pause and look broken.
  5. Rule not firing? Check rules/rulesRegistry.ts to confirm it's registered and enabled, then check the rule's own matching logic — it may just not match your test input's exact shape.
  6. Repeated log lines for one save? That's the save-loop bug resurfacing — check that the internal-save guard in pipeline/savePipeline.ts hasn't been bypassed by a recent edit.

11. How to Maintain / Extend It

Adding a new text-level rule:

  1. Check native/nativeCleanupMap.ts (Section 7.1) — is it already native? If yes, stop, just enable the setting.
  2. Find the corresponding Eclipse fix class for behavioral reference (Section 14 has the confirmed paths).
  3. Decide: text-safe, or needs an AST helper? If it needs real type/scope info, document it in research/stsActionMap.ts as a future gap — don't force it into a text rule.
  4. If text-safe: create the file under text/, implement the KbRule contract from rules/rulesTypes.ts, register it in rules/rulesRegistry.ts.
  5. Test: build a fixture input/expected-output pair, run it, confirm idempotence (running the rule twice on clean output makes no further changes).
  6. Update Section 7.2 of this doc, the README, and research/stsActionMap.ts.
  7. Bump the minor version in package.json — a new rule is always at least a minor bump.

When Red Hat updates vscode-java: re-check document/_java.learnMoreAboutCleanUps.md against native/nativeCleanupMap.ts. If a new native action covers something we built ourselves, remove the KB rule and switch to native — don't run duplicate implementations of the same cleanup.

When Eclipse JDT updates: Eclipse is actively migrating cleanup logic between bundles (confirmed: ControlStatementsFix.java, StringFixCore.java, and UnusedCodeFixCore.java all currently live in org.eclipse.jdt.core.manipulation, not org.eclipse.jdt.ui, where some older references might assume). Re-check the specific fix class for any rule you've ported when upgrading your reference checkout — file locations have moved before and may move again.

Versioning policy:

  • New rule or behavior change → minor bump
  • Bug fix in an existing rule → patch bump
  • Breaking architectural change → major bump

What this extension does not claim

  • Don't claim the text rules are equivalent to Eclipse's AST-backed versions.
  • Don't claim full STS parity — this is progressive improvement, not 100%.
  • Don't claim it works in managed VS Code without the F5/portable workaround.

How to handle limitations honestly without killing the demo

Frame every gap as "known and tracked," not "broken." The research/gap-analysis layer exists specifically so this framing is true, not just a talking point.


12. Future Scope

  • Add more custom rules, but only where native Red Hat cleanup doesn't already cover the case — check Section 7.1 first, every time.
  • Build a small fixture-test set (input/expected-output Java files) per rule, so testing isn't just "save something messy and eyeball it."
  • Keep Section 7's native-vs-custom mapping current as Red Hat adds more cleanup actions over time.
  • For real STS parity on the harder rules: evaluate an AST-backed approach — most realistically a small Java helper process using actual JDT parsing, invoked per-file (not a whole-module OpenRewrite-style run, to avoid reintroducing the speed problem from Section 3.3).
  • Decide, as a team, whether v2 stays a dev/demo tool or becomes a supported internal extension — that decision is blocked on the company VS Code extension policy question, not on the code.

13. References

All paths below were verified directly against a real checkout of each repo — not assumed from memory.

Source What it grounds
redhat-developer/vscode-java — document/_java.learnMoreAboutCleanUps.md The authoritative list of all 20 native cleanup action IDs. Every ID in Section 7.1 was counted and verified directly against this file.
redhat-developer/vscode-java — issue #557 Active-editor-mismatch guard pattern, mirrored in pipeline/documentRefresh.ts.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpPostSaveListener.java Eclipse's own "run cleanups on save" orchestration — the closest real-world equivalent of pipeline/savePipeline.ts. Confirmed still located in the org.eclipse.jdt.ui bundle.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java Confirmed still in org.eclipse.jdt.ui.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java Source of truth for Eclipse's cleanup constant names/catalog. Confirmed located in org.eclipse.jdt.core.manipulation, not org.eclipse.jdt.ui — this bundle is the headless-capable split-out of cleanup logic, an important distinction if tracing source paths yourself.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/ControlStatementsFix.java Behavioral reference for kb.controlStatementBlock. Confirmed in org.eclipse.jdt.core.manipulation.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/StringFixCore.java Reference for StringBuilder/StringBuffer conversion (Section 7.3 gap). Confirmed in org.eclipse.jdt.core.manipulation.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/UnusedCodeFixCore.java Reference for unnecessary cast detection (Section 7.3 gap). Confirmed in org.eclipse.jdt.core.manipulation.
eclipse-jdt/eclipse.jdt.ui — org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/RedundantModifiersCleanUp.java Source confirmation for the redundantModifiers native action's Eclipse-side equivalent. Confirmed in org.eclipse.jdt.core.manipulation.
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft