A Java productivity layer for Neovim, on top of an externally managed jdtls.
Code generation · class creation · test runner · build runner · refactorings · debugging
jc.nvim never starts or installs the language server. You run jdtls with
nvim-java,
nvim-jdtls or nvim-lspconfig, and
jc.nvim hooks into whatever jdtls client attaches and adds the ergonomics of
vim-javacomplete2 (its
predecessor), rebuilt on Neovim's built-in LSP client.

toString, hashCode/equals, constructors,
accessors, all with interactive field selection; add unimplemented
(abstract) methods.Get →
Getter/GetMapping/…, picks and imports the chosen type; Service spring
narrows same-named types by package) — a live telescope picker when
available, otherwise a prompt + vim.ui.select.<Tab>
completion, project-aware package/module resolution and a library of
templates (records, spring stereotypes, JPA entity, JUnit, …).a.equals(b) → b.equals(a)), move
a class to another package (references updated).gf).javap / jshell / jol; decompiled
jdt:// class view; wipe a corrupted jdtls workspace.Generating a constructor and toString — the picker windows let you pick the
fields and the style:

Adding an annotation by name — a live search over jdtls' types inserts the
@Annotation and its import:

Replacing an import — pick a different same-named type and the import line is swapped in place:

jdtls from nvim-java, nvim-jdtls or lspconfig, started with
extendedClientCapabilities (notably executeClientCommandSupport and
advancedOrganizeImportsSupport) — nvim-java and nvim-jdtls do this out of
the box.init_options.bundles) and nvim-dap or vimspector.:JCutilJol downloads the jol-cli jar into ~/.m2 on first use.return {
"artur-shaik/jc.nvim",
ft = { "java" },
dependencies = { "nvim-java/nvim-java" },
opts = {
keys_prefix = "<leader>j",
},
}
{
"artur-shaik/jc.nvim",
ft = { "java" },
dependencies = {
"nvim-java/nvim-java",
{
"nvim-neotest/neotest",
optional = true,
dependencies = { "nvim-neotest/nvim-nio", "nvim-lua/plenary.nvim" },
opts = function(_, opts)
opts.adapters = opts.adapters or {}
table.insert(opts.adapters, require("jc").neotest_adapter())
-- optional: auto-close the summary on an all-green focused run
opts.consumers = opts.consumers or {}
opts.consumers.jc = require("jc").neotest_consumer()
end,
},
},
opts = { keys_prefix = "<leader>j" },
}
jc.nvim works with any owner of the jdtls client — nvim-jdtls or a plain
lspconfig setup are fine too; just drop nvim-java from dependencies and
start jdtls your own way.
If setup is never called, opening a java file initializes the plugin with
defaults.
All options go through setup(opts) (or your plugin manager's opts):
require("jc").setup({
keys_prefix = "<leader>j", -- prefix for the default mappings
default_mappings = true, -- install default mappings on attach
autoformat_on_save = false, -- format java buffers on save
debug_backend = nil, -- "dap" | "vimspector" | nil (auto-detect)
basedir = nil, -- data dir, default ~/.local/share/jc.nvim
update_config_on_new_file = true, -- refresh jdtls build path on new java files
templates_dir = nil, -- dir of user class templates
class_type_exclude = nil, -- package prefixes hidden from type completion
class_prompt = "oneline", -- "oneline" (DSL) | "wizard" (step-by-step)
map_gf = true, -- override gf with an FQN-aware go-to-file
on_attach = nil, -- function(client, bufnr) extra hook
test = { -- test runner (see Test runner)
precompile = false, -- compile with gradle/maven before a run
build_java_home = nil, -- JDK for that build (default: the project's; false = inherit)
notify = true, -- toast run start / result
open_summary = true, -- open the neotest summary on a run
autoclose_summary = true, -- close it after an all-green focused run
console_launcher_path = nil, -- path to the JUnit console-standalone jar
debug = nil, -- test debugger: nil = jc-native, "external" = nvim-jdtls/nvim-java
},
})
Swaps the one-line DSL prompt for a step-by-step vim.ui.select/vim.ui.input
flow (template → module → package → name → extends/implements/fields/flags).
Each step is a short clean list, which avoids the cmdline-completion truncation
of very long package paths. The mapping <p>N always runs the wizard,
regardless of this option.
Adds package prefixes to hide from the extends/implements/field-type
completion. The prompt resolves types from jdtls' workspace symbols, which
include non-importable ones; nested classes, shaded jars, internal/impl
packages and a built-in list of known JDK/library internals (sun.*,
com.sun.*, jdk.internal, jackson introspect/cfg/…) are dropped
automatically. The LSP gives no visibility, so package-private classes in
ordinary packages can still slip through — add their prefixes here, e.g.
{ "com.example.somelib.internalish" }.
A java file created in-editor isn't on jdtls' build path until the project
configuration is refreshed, so go-to-definition returns nothing on it (while
find-references still works off the search index). With this on (default), jc
detects such files and fires :JCutilUpdateConfig on their first write. Set it
to false to refresh manually.
g:jc_default_mappings, g:jc_autoformat_on_save, g:jc_debug_backend and
g:jc_basedir still work as a fallback when the corresponding option isn't
passed to setup.
:checkhealth jc verifies the setup; :help jc has the full reference.
Imports & code generation
| Command | Action |
|---|---|
JCimportsOrganizeSmart |
organize imports, auto-picking remembered classes |
JCimportsOrganize |
organize imports, choosing from the candidate list (and forgetting the remembered pick, which is how you revise it) |
JCimportsReplace |
replace the import of the type under the cursor (pick among same-named, e.g. lombok.Value vs spring's) |
JCimportsRemoveUnused |
remove all unused imports (no reordering) |
JCimportsAddMissing |
add all missing imports (no reordering; smart-picks ambiguous names) |
JCimportsOrganizeNoSort |
add missing + remove unused, without reordering the rest |
JCimportsStyle |
pick an IDE import-sort preset (Eclipse / IntelliJ IDEA / VS Code / Google), remembered per project |
A pick for an ambiguous name is remembered per project (a file next to the
jdtls workspace), and the smart commands above apply it without asking again.
Organizing the imports of a newly created class counts as a smart run, so a
javax vs jakarta choice is made once, not on every new class. One class is
remembered per simple name: picking another one, in the picker or with
JCimportsReplace, replaces it rather than piling up next to it.
| JCgenerateToString | generate toString() with field selection |
| JCgenerateHashCodeAndEquals | generate hashCode() and equals() |
| JCgenerateAccessors | choose fields for accessor generation |
| JCgenerateAccessorGetter / …Setter / …SetterGetter | getter / setter / both for a field |
| JCgenerateConstructor | choose fields for a constructor |
| JCgenerateConstructorDefault | no-arg constructor |
| JCgenerateAbstractMethods | add unimplemented methods |
Class creation & navigation
| Command | Action |
|---|---|
JCgenerateClass |
class creation prompt (DSL or wizard per class_prompt) |
JCgenerateClassWizard |
class creation, always the step-by-step wizard |
JCgenerateClassFromCursor |
create the class named under the cursor (pick package/module, then the DSL) |
JCgotoTest |
jump to the test class (or back), creating it if missing |
JCgotoFqn |
open the java file for the FQN under the cursor |
Refactor
| Command | Action |
|---|---|
JCrefactorExtractVar |
extract variable (all occurrences) |
JCrefactorExtractMethod |
extract method (visual range) |
JCrefactorStaticImport |
convert the call at the cursor to a static import |
JCrefactorStaticImportEnum |
static-import every constant of the enum |
JCrefactorFlipArgs |
swap receiver and argument of the call at the cursor (a.equals(b) → b.equals(a)) |
JCrefactorMove |
move the current class to another package/source root, updating references |
JCannotateMethod / JCannotateClass |
add an annotation to the enclosing method / class (search jdtls by name, import remembered); type Service spring to narrow same-named types by package |
Test runner
| Command | Action |
|---|---|
JCtestRun |
run the test at the cursor |
JCtestFile |
run every test in the current file |
JCtestDebug |
debug the test at the cursor (delegates to nvim-jdtls / nvim-java) |
JCtestSuite |
run every test under the project root |
JCtestPick |
pick a test class from the whole project and run it |
JCtestLast |
re-run the last test position |
JCtestStop |
stop the running test |
JCtestSummary / JCtestOutput |
toggle summary / open the test's output |
JCtestPrecompile |
toggle build-tool precompile before a run |
JCtestInstall |
download the JUnit console launcher matching the project's junit (optional version argument) |
Build runner
| Command | Action |
|---|---|
JCbuildRun [args] |
run gradle/maven with args (or prompt, defaulting to the last run) |
JCbuildTask |
pick a module then a task/goal |
JCbuildLast |
repeat the last build task |
Debug & utilities
| Command | Action |
|---|---|
JCdebugAttach / JCdebugLaunch |
attach / launch the debugger |
JCdapAttach / JCvimspectorAttach |
attach with a specific backend |
JCdebugWithConfig |
start with a chosen vimspector configuration |
JCtoggleAutoformat |
toggle format-on-save |
JCutilUpdateConfig |
re-read the project configuration (pom/gradle) |
JCutilWipeWorkspace |
delete the jdtls workspace and restart (works even if jdtls failed to start) |
JCutilFixJdk |
point jdtls' gradle sync at the JDK the build targets (reported by :checkhealth jc) |
JCutilJshell |
java shell with the project classpath |
JCutilBytecode |
bytecode of the current class (javap) |
JCutilJol |
object layout (jol) |
Installed on jdtls attach when default_mappings is enabled. <p> is
keys_prefix (default <leader>j).
| Mode | Keys | Action |
|---|---|---|
| n | <p>i / <p>I |
organize imports — smart (sorted) / no reordering |
| n | <p>ro |
pick an IDE import-sort preset, remembered per project |
| i | <C-j>i |
organize imports |
| n | <p>ts |
toString() |
| n | <p>eq |
hashCode() and equals() |
| n | <p>A |
accessors (field selection) |
| n | <p>s / <p>g / <leader>ja |
setter / getter / both |
| i | <C-j>s / <C-j>g / <C-j>a |
accessor generation |
| n | <p>c / <p>cc |
constructor (fields) / default constructor |
| n | <p>m, i <C-j>m |
abstract methods |
| n | <p>n / <p>N |
new class — prompt / wizard |
| n | <p>nc |
create the class named under the cursor (missing from the project) |
| n | <p>t |
jump to the test class (or back) |
| n | gf |
go to file, or the java file of the FQN under the cursor |
| n | <p>Tr / <p>Tf / <p>Ta / <p>Tl |
run test at cursor / file / all / last |
| n | <p>Td |
debug the test at the cursor (via nvim-jdtls / nvim-java) |
| n | <p>Tp |
pick a test class from the project and run it |
| n | <p>Ts / <p>To |
toggle test summary / open test output |
| n | <p>b / <p>B |
run gradle/maven (prompt) / pick a task |
| n | <p>da / <p>dl |
debug attach / launch |
| v | <p>re / <p>rm |
extract variable / method (selection) |
| n | <p>re |
extract variable, all occurrences (at cursor) |
| n | <p>rs / <p>rS |
static import — call / every enum constant |
| n | <p>rp |
replace the import of the type under the cursor |
| n | <p>ru / <p>ri |
remove unused / add missing imports (no reordering) |
| n | <p>rf |
flip receiver and argument of the call (a.equals(b) → b.equals(a)) |
| n | <p>rM |
move the current class to another package (references updated) |
| n | <p>am / <p>ac |
add an annotation to the enclosing method / class |
:JCgenerateClass (<p>n) opens a one-line prompt. The scheme, slot by slot:
template : [subdir] : /package.ClassName extends X implements Y permits Z (fields) :flags
└── 1 ─┘ └── 2 ─┘ └─────── 3 ──────┘ └──────── 4 ─────────┘ └── 5 ─┘ └ 6 ┘
| # | Slot | Meaning |
|---|---|---|
| 1 | template: |
(optional) a template — record, entity, service, junit5, … (see Templates) |
| 2 | [subdir]: |
(optional) a source-set or subproject (see below) |
| 3 | /package.Name |
class name and package. Leading / = absolute in the source root; without it, relative to the current file's package |
| 4 | extends/implements |
(optional) supertypes, imported automatically |
| 4b | permits |
(optional) sealed subtypes; implies the sealed modifier |
| 5 | (fields) |
(optional) type name, comma-separated, private by default. Drop the name and it is derived from the type (final RiTypeEvent → private final RiTypeEvent riTypeEvent). For enum this slot lists the constants |
| 6 | :flags |
(optional) code-gen and lombok flags (see below) |
Everything except the class name is optional — /com.app.User alone makes an
empty class.
Absolute (leading /) — the package is taken literally:
| Prompt | Creates |
|---|---|
/com.app.User(String name, int age) |
a User class with two fields |
/com.app.User(String name):constructor:toString |
…plus an all-args constructor and toString |
record:/com.app.Point(int x, int y) |
a record Point(int x, int y) |
entity:/com.app.Order(String number) |
an @Entity with an @Id id and @Column fields |
interface:/com.app.OrderRepo extends CrudRepository |
an interface extending CrudRepository |
enum:/com.app.Status(NEW, PAID, SHIPPED) |
an enum with those constants |
sealed:/com.app.Shape permits Circle, Square |
a sealed interface naming its subtypes |
service:/com.app.OrderService |
an @Service class |
/com.app.UserDto(String id, String name):lombokData |
a class annotated @Data |
service:/com.app.JobRunner(final RiTypeEvent, final DictionaryService) |
an @Service whose fields are named after their types |
[test]:/com.app.UserTest |
a class under src/test/java |
[core]:/com.app.Foo |
a class in the core module (multi-module) |
Relative (no leading /) — the package is resolved against the current
file's package. Editing com.app.service.OrderService:
| Prompt | Creates |
|---|---|
Helper |
com.app.service.Helper |
Helper(String name) |
…with a field |
util.Strings |
com.app.service.util.Strings (a sub-package) |
record:Money(long amount) |
com.app.service.Money from the record template |
Trailing :flag segments run after the class is created. Code generation
flags go through jdtls:
| Flag | Generates |
|---|---|
constructor |
an all-fields constructor |
toString |
toString() |
hashCode |
hashCode() |
equals |
equals() |
On a record these four are skipped, and so is the accessor step that otherwise
runs whenever a class is created with fields: the compiler writes all of them
for the components. Same for the accessors on an interface or @interface.
Lombok flags add the annotation (and its import, resolved by organize-imports) instead of generating code:
| Flag | Annotation | Flag | Annotation | |
|---|---|---|---|---|
lombok / lombokData |
@Data |
lombokNoArgs |
@NoArgsConstructor |
|
lombokValue |
@Value |
lombokAllArgs |
@AllArgsConstructor |
|
lombokBuilder |
@Builder |
lombokRequiredArgs |
@RequiredArgsConstructor |
|
lombokGetter |
@Getter |
lombokToString |
@ToString |
|
lombokSetter |
@Setter |
lombokEqualsHashCode |
@EqualsAndHashCode |
|
lombokSlf4j |
@Slf4j |
Flags combine: /com.app.User(String name, int age):lombokData:lombokBuilder
→ a @Data @Builder class.
[subdir])src/<name>/java
— [test] mirrors the package into src/test/java;[core] or [core/test] for its test sources.When an absolute package you pick already lives in another module (completion offers packages from every subproject), jc asks which module to create the class in; a brand-new package goes to the current module.
<Tab> completes each slot in turn:
/, existing packages across the whole project
(every subproject) — without a leading /, the sub-packages of the current
file's package instead; either way you can still type a new package by hand;[subdir] after a template — source-sets and module names;extends/implements — class/interface names resolved live from
jdtls.<p>N (or class_prompt = "wizard") runs the same thing as a step-by-step
vim.ui flow instead of the one-liner.
With the cursor on a class name the code refers to but that doesn't exist yet,
<p>nc (:JCgenerateClassFromCursor) picks up that name, asks for a package
(every existing project package, the current one, or a new one — and the module
on a multi-module project), then drops you in the DSL prompt pre-filled with
[module]:/pkg.Name so you can still add extends, fields or flags before
creating it.
Built-in: class, interface, enum, record, annotation, exception,
main, singleton, serializable, sealed, sealed_class, servlet,
junit, junit5, entity, service, component, repository, controller
and the android_* family.
The entity template carries @Entity and an @Id id, and annotates each
prompt field with @Column(name = "<snake_case>"). Imports are left to
organize-imports (run automatically after creation), so it works whether your
project uses jakarta.* or javax.*.
The controller template maps the path its name implies
(UserController → @RequestMapping("/user")), servlet maps MyFileServlet
to /my-file, exception declares the four conventional constructors, and
junit/junit5 scaffold a @Test. Fields given to interface or annotation
become members (String name();), not fields.
sealed and sealed_class produce a sealed interface or class; name the
subtypes with permits, and the wizard asks for them as an extra step. A
permits clause on a plain interface/class adds the sealed modifier by
itself, since the two only make sense together.
The serializable template implements Serializable and declares a freshly
generated serialVersionUID above the fields. An implements given in the DSL
is merged with the template's own, so
serializable:/com.app.Money implements Comparable<Money> still keeps
Serializable.
The repository template is a spring-data interface over the entity its
name implies — repository:/com.app.UserRepository gives
public interface UserRepository extends JpaRepository<User, Long> (a trailing
Repository/Repo is stripped). There is no @Repository: spring-data builds
the bean from the interface. Write your own supertype to override it, e.g.
repository:/com.app.UserRepository extends CrudRepository<User, UUID>.
Point templates_dir at a folder of <name>.lua files. Each returns either
a declarative spec table (recommended — describe only the essence, the engine
builds the rest) or a function(opts) -> string for full control.
A Lombok DTO is just imports + an annotation, no skeleton to repeat:
-- ~/.config/nvim/jc-templates/dto.lua
return {
imports = { "lombok.Data" },
annotations = { "@Data" },
}
dto:/com.app.User(String name, int age) then produces a @Data class with the
package, declaration and fields filled in.
Spec fields (all optional): kind (class/interface/enum/annotation/
record), modifiers, extends, implements, imports, annotations,
body, pre_fields (members before the prompt fields), field_annotation
(function(field) -> string). imports/annotations/body may each be a
string, a list or a function(opts). User input for extends/implements
overrides the spec defaults. opts: name, package, fields
({ mod, type, name }), extends, implements.
jc.nvim ships an optional set of Java field/modifier and NetBeans-style
snippets (snippets/java.json, VS Code format). jc doesn't run a snippet engine
— point your own at the folder. The prefix scheme: p/P = private/public,
s = static, f = final; a lowercase type initial is a primitive (psfl →
private static final long), an uppercase one a wrapper (psfL → … Long).
Plus fori, forl, ife, dowhile, whileit, inst, pst, soutv,
runn, lazy.
Point the loader at the plugin's snippets/ directory (adjust the path to your
plugin manager; the lazy.nvim location is shown):
-- LuaSnip
require("luasnip.loaders.from_vscode").lazy_load({
paths = { vim.fn.stdpath("data") .. "/lazy/jc.nvim/snippets" },
})
-- nvim-cmp + vsnip, blink.cmp, or native vim.snippet users: load the same
-- VS Code snippet folder however your engine consumes `package.json` bundles.
jc.nvim ships a neotest adapter.
neotest is an optional dependency — without it the plugin works as before
and the JCtest* commands warn instead of erroring. Unlike the gradle/maven
adapters, this one resolves the test classpath straight from jdtls and runs the
JUnit Platform Console Standalone
launcher, so there's no build-tool daemon to wait for and gradle/maven/plain
layouts all work the same way. Wire it as in
Installation.

The launcher jar is looked up in ~/.m2; if missing, run :JCtestInstall once
(downloads org.junit.platform:junit-platform-console-standalone via maven) or
set test.console_launcher_path.
The jar bundles its own JUnit engines and they take precedence over the
project's, so jc picks the version matching the junit on the test classpath:
junit 5.11.3 needs the 1.11.3 jar, junit 6.0.3 needs 6.0.3 (junit 6 merged the
two numbering schemes). Running a JUnit 6 project on a 1.x jar fails inside the
framework - typically NoSuchMethodError from SpringExtension. :JCtestInstall
fetches the version the current project needs; pass one explicitly with
:JCtestInstall 6.0.3. A test.console_launcher_path always wins.
The download goes through maven when it is on PATH, and falls back to a direct
fetch from Maven Central (curl or wget) when maven cannot get the jar - a
corporate mirror in settings.xml intercepts every maven request and such
mirrors often lag behind on new artifacts. Progress is echoed in the cmdline.
Run tests with :JCtestRun (cursor), :JCtestFile, :JCtestSuite,
:JCtestPick, :JCtestLast, or the <p>T* mappings; neotest paints the gutter
green/red and a failed test's diagnostic points at the failing line. Runs open
the summary panel and, via the optional jc consumer, auto-close it after an
all-green focused run (cursor/file/class) — runs with failures stay open.
JCtestRun/JCtestFile also work from a production class: they run its paired
<Class>Test (the same counterpart JCgotoTest uses) when it exists.
The adapter toasts running… at the start and N passed, M failed, K skipped
at the end. Knobs: test.notify, test.open_summary, test.autoclose_summary
(false, or a delay in ms).
Before a focused run jc reloads the target file and has neotest reparse it, so
a test written to disk from outside the editor (an agent, a git checkout) is
picked up without saving the buffer by hand — neotest itself rediscovers a file
only on BufAdd/BufWritePost. A buffer with unsaved changes is left alone,
and the reparse needs the jc consumer wired in (that is where the neotest
client comes from). :JCtestSuite is not refreshed: it runs off the tree as it
stands, so a brand-new test file joins it only once neotest rescans the
directory.
Debugging tests — :JCtestDebug (<p>Td) debugs the test at the cursor.
Set your breakpoints first. Needs nvim-dap
and the java-debug bundle in jdtls
(nvim-java bundles it).
By default jc runs its own debugger: it launches the JUnit Platform Console Launcher under a JDWP agent (same classpath and JDK as a normal run) and attaches nvim-dap to it, then reports pass/fail from the XML. Because the console launcher is standalone (its own junit-platform), this works on any project regardless of junit version — including ones where the delegated runner below silently finds 0 tests.
Set test.debug = "external" to delegate to
nvim-jdtls
(jdtls.dap.test_nearest_method) or
nvim-java
(test.debug_current_method) instead — you get their report UI, but their
eclipse test runner bundles its own junit (~5.11) and can discover 0 tests on
a project whose junit differs from the bundle's. "external" falls back to
jc-native when neither plugin is installed.
The classpath is built from jdtls and augmented for correctness:
test + runtime scopes are unioned — jdtls' test scope omits
runtimeOnly dependencies ByteBuddy/Mockito need at run time (otherwise
"green from the CLI but NoClassDefFoundError here").
By default (precompile = false) jc forces a jdtls compile
(java/buildWorkspace), uses jdtls' bin output first and the gradle/maven
build/-target/ dirs as a fallback. Fast, and fine when jdtls compiles the
whole project.
Some projects have classes jdtls won't put in bin (e.g. certain spring-data
repositories) — the run then fails with ClassNotFoundException for a class
that exists in the build output. Set test.precompile = true (or toggle with
:JCtestPrecompile): jc runs gradle :<module>:testClasses /
mvn test-compile first and uses the complete build/-target/ output. The
compile is async (editor stays responsive, progress in the cmdline), cached
per module for the run, and on failure the javac/maven errors go to the
quickfix list instead of running the tests.
That build runs on the project's JDK (the same one the tests launch with),
not on whatever JAVA_HOME nvim inherited — an older gradle on a JDK 16+
otherwise dies with IllegalAccessError: ... jdk.compiler does not export com.sun.tools.javac.*. Override with test.build_java_home = "/path/to/jdk",
or set it to false to keep nvim's environment.
The run JVM is the configured java.configuration.runtimes entry matching the
highest bytecode version among the module's classes (a 17-compiled test
over an 11-target main still runs on 17, as gradle does), falling back to
resolveJavaExecutable then PATH java.
jdtls compiles with the Eclipse compiler, whose equivalent of javac
-parameters is off by default, so classes in bin carry no method parameter
names. Spring Data reads those names by reflection, so a repository with
named query parameters that works from the CLI fails here with "For queries
with named parameters you need to provide names for method parameters". Fix it
in the project with @Param("...") (compiler-independent), or turn the flag on
for jdtls - org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
in the module's .settings/org.eclipse.jdt.core.prefs, or in the file
java.settings.url points at. precompile = true sidesteps it too, since the
bytecode then comes from javac.
:checkhealth jc ends with the project's java levels: what the build declares,
what eclipse compliance jdtls settled on, and what level the build output
actually is. When they disagree it says how that bites and what to do:
:JCutilFixJdk rewrites java.home in the project's
.settings/org.eclipse.buildship.core.prefs after a confirmation; re-sync
with :JCutilUpdateConfig afterwards.compileJava { sourceCompatibility = 11 } instead of java { sourceCompatibility = 11 } -
and jc says so when it recognises that shape. It never rewrites build.gradle
itself.bin and build/classes sit at different levels, which is what the test
runner has to launch over.If jdtls keeps dropping classes from bin, a :JCutilWipeWorkspace + restart
(clean re-import) often makes bin complete again, keeping you on the fast
precompile = false path.
On a multi-module project :JCtestSuite is best-effort: neotest reruns
update_running over the shared tree per sub-run, which can reset an
already-failed class back to running in the summary. Iterate with the focused
:JCtestRun/:JCtestFile, which run as a single neotest run and report
reliably.
Run gradle/maven tasks from the editor, in a dedicated split (q closes it);
compile errors are parsed into the quickfix list.
:JCbuildRun [args] — run with the given args, or prompt (defaulting to the
last run, remembered per project). Wide pty so long file:line: errors aren't
wrapped.:JCbuildTask — pick a module (or the whole project), then a task: gradle
tasks from gradlew tasks, or for maven the lifecycle phases, pom
profiles/plugin goals and a plugin drill-down (mvn help:describe lists every
goal of the chosen plugin). The module scopes the run (gradle :module:task,
maven -pl module -am).:JCbuildLast — repeat the last task.Commands run from the reactor root (outermost contiguous pom / settings.gradle), so multi-module builds resolve paths and the reactor correctly.
JCgotoFqn (and the overridden gf) opens the java source for a
fully-qualified name under the cursor — for jumping out of a terminal, a neotest
output window or a pasted stack trace into the code. It understands:
com.foo.Bar (and com.foo.Bar$Inner → the outer file);com.foo.Bar.method → the Bar file;com.foo.Bar:42;at com.foo.Bar.method(Bar.java:25) → Bar, line 25 (rejoined
even when a narrow terminal wrapped it across two lines).
The file opens in the last window that showed a java buffer (so you can trigger it from a terminal split and land back in your editing window), or a new tab when there is none. The FQN is resolved through jdtls' symbol index (works from any buffer) with a source-tree fallback.
When default_mappings is on, gf is overridden globally and falls back to the
builtin gf when the token isn't an FQN (e.g. a real path). Disable with
setup{ map_gf = false }.
JCdebugAttach / JCdebugLaunch route to a backend:
debug_backend option / g:jc_debug_backend if set ("dap" or
"vimspector");Attach asks for host and port, remembered per project. The adapter port is
resolved from jdtls via vscode.java.startDebugSession, which needs the
java-debug bundle.
| Feature | nvim-jdtls | jc.nvim |
|---|---|---|
| Code generation | via code actions | dedicated commands/mappings with field selection |
| Organize imports | code action | smart mode remembering preferred classes per project |
| Class creation | — | DSL prompt / wizard with templates |
| Test runner | — | neotest adapter, classpath from jdtls |
| Build runner | — | gradle/maven task picker → quickfix |
| Debug attach | manual dap config | JCdebugAttach with per-project host/port memory |
| javap/jshell/jol | yes | classpath-aware, built-in |
:checkhealth jc — it verifies the Neovim version, the attached jdtls
client, organize-imports and java-debug availability, the debug backends,
classpath resolution and neotest/launcher for the test runner, the jol jar,
the treesitter java parser and the data dir.update_config_on_new_file handles this on first write, or
run :JCutilUpdateConfig.:JCutilWipeWorkspace
deletes the eclipse index and restarts (works even with no client attached).ClassNotFoundException for classes that exist — enable
test.precompile (see Test runner).