From 05b827d2b86ebc155aa3bffb144475772c176bf4 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 14:31:42 +0530 Subject: [PATCH 01/27] feat: v4.0.0 --- .devcontainer/config/nvim/init.lua | 126 +++++ .devcontainer/config/nvim/lazy-lock.json | 6 + .devcontainer/devcontainer.json | 21 + .devcontainer/setup.sh | 5 + .github/workflows/test.yml | 7 +- .gitignore | 2 +- .luacheckrc | 3 + .luarc.json | 4 + .stylua.toml | 2 +- .vscode/tasks.json | 15 +- CLAUDE.md | 153 ++++++ Makefile | 21 +- README.md | 498 +----------------- doc/development.md | 51 ++ doc/server-capabilities.md | 164 ++++++ doc/ts-to-lua-guide.md | 35 ++ lazy.lua | 15 - lua/async/runner.lua | 63 +++ lua/async/waits/wait.lua | 12 + lua/async/waits/wait_all.lua | 35 ++ lua/async/waits/wait_with_error_handler.lua | 20 + lua/async/wrap.lua | 20 + lua/java-core/constants/java_version.lua | 14 + .../ls/clients/java-debug-client.lua | 88 ++++ lua/java-core/ls/clients/java-test-client.lua | 112 ++++ lua/java-core/ls/clients/jdtls-client.lua | 385 ++++++++++++++ lua/java-core/ls/servers/jdtls/cmd.lua | 133 +++++ lua/java-core/ls/servers/jdtls/conf.lua | 33 ++ lua/java-core/ls/servers/jdtls/env.lua | 32 ++ lua/java-core/ls/servers/jdtls/init.lua | 26 + lua/java-core/ls/servers/jdtls/plugins.lua | 66 +++ lua/java-core/ls/servers/jdtls/root.lua | 29 + lua/java-core/types/jdtls-types.lua | 11 + lua/java-core/types/nvim-types.lua | 6 + lua/{java => java-core}/utils/buffer.lua | 0 lua/java-core/utils/class.lua | 280 ++++++++++ lua/java-core/utils/command.lua | 53 ++ .../utils/error_handler.lua} | 8 +- lua/java-core/utils/errors.lua | 14 + lua/java-core/utils/event.lua | 23 + lua/java-core/utils/file.lua | 15 + lua/java-core/utils/list.lua | 185 +++++++ lua/{java => java-core}/utils/log.lua | 33 +- lua/java-core/utils/log2.lua | 147 ++++++ lua/java-core/utils/lsp.lua | 66 +++ lua/java-core/utils/notify.lua | 22 + lua/java-core/utils/path.lua | 16 + lua/java-core/utils/set.lua | 22 + lua/java-core/utils/system.lua | 45 ++ lua/java-dap/data-adapters.lua | 44 ++ lua/java-dap/init.lua | 74 +++ lua/java-dap/runner.lua | 72 +++ lua/java-dap/setup.lua | 125 +++++ lua/java-refactor/action.lua | 303 +++++++++++ lua/java-refactor/api/build.lua | 19 + lua/java-refactor/api/refactor.lua | 38 ++ lua/java-refactor/client-command-handlers.lua | 129 +++++ lua/java-refactor/client-command.lua | 93 ++++ lua/java-refactor/init.lua | 46 ++ lua/java-refactor/refactor.lua | 359 +++++++++++++ lua/java-refactor/utils/error_handler.lua | 9 + lua/java-refactor/utils/instance-factory.lua | 12 + .../runner => java-runner}/run-logger.lua | 0 lua/{java/runner => java-runner}/run.lua | 10 +- lua/{java/runner => java-runner}/runner.lua | 26 +- lua/java-test/adapters.lua | 42 ++ lua/java-test/api.lua | 142 +++++ lua/java-test/init.lua | 87 +++ lua/java-test/reports/junit.lua | 73 +++ lua/java-test/results/execution-status.lua | 7 + lua/java-test/results/message-id.lua | 30 ++ .../results/result-parser-factory.lua | 13 + lua/java-test/results/result-parser.lua | 196 +++++++ lua/java-test/results/result-status.lua | 7 + lua/java-test/ui/floating-report-viewer.lua | 88 ++++ lua/java-test/ui/report-viewer.lua | 12 + lua/java-test/utils/string-builder.lua | 30 ++ lua/java.lua | 138 ++--- lua/java/api/dap.lua | 58 -- lua/java/api/profile_config.lua | 29 +- lua/java/api/runner.lua | 14 +- lua/java/api/settings.lua | 32 +- lua/java/api/test.lua | 86 --- lua/java/config.lua | 100 ++-- lua/java/dap/init.lua | 112 ---- lua/java/startup/decompile-watcher.lua | 12 +- lua/java/startup/duplicate-setup-check.lua | 27 - lua/java/startup/exec-order-check.lua | 48 -- lua/java/startup/lsp_setup.lua | 43 ++ lua/java/startup/lspconfig-setup-wrap.lua | 52 -- lua/java/startup/mason-dep.lua | 105 ---- lua/java/startup/mason-registry-check.lua | 52 -- lua/java/startup/nvim-dep.lua | 67 --- lua/java/startup/startup-check.lua | 53 -- lua/java/treesitter/init.lua | 26 - lua/java/treesitter/queries.lua | 15 - lua/java/ui/profile.lua | 98 ++-- lua/java/{utils/ui.lua => ui/utils.lua} | 6 +- lua/java/utils/command.lua | 25 - lua/java/utils/instance_factory.lua | 24 - lua/java/utils/jdtls.lua | 17 - lua/java/utils/jdtls2.lua | 21 - lua/java/utils/mason.lua | 108 ---- lua/pkgm/downloaders/factory.lua | 40 ++ lua/pkgm/downloaders/powershell.lua | 69 +++ lua/pkgm/downloaders/wget.lua | 63 +++ lua/pkgm/extractors/factory.lua | 77 +++ lua/pkgm/extractors/powershell.lua | 65 +++ lua/pkgm/extractors/tar.lua | 57 ++ lua/pkgm/extractors/uncompressed.lua | 47 ++ lua/pkgm/extractors/unzip.lua | 39 ++ lua/pkgm/manager.lua | 183 +++++++ lua/pkgm/pkgs/jdtls.lua | 0 lua/pkgm/specs/base-spec.lua | 220 ++++++++ lua/pkgm/specs/init.lua | 78 +++ lua/pkgm/specs/jdtls-spec/init.lua | 42 ++ lua/pkgm/specs/jdtls-spec/version-map.lua | 22 + plugin/java.lua | 42 +- tests/constants/capabilities.lua | 79 +++ tests/prepare-config.lua | 66 --- tests/specs/capabilities_spec.lua | 28 + tests/specs/jdtls_extensions_spec.lua | 34 ++ tests/specs/lsp_spec.lua | 14 + tests/test-config.lua | 26 - tests/utils/lsp-utils.lua | 25 + tests/utils/prepare-config.lua | 56 ++ tests/utils/test-config.lua | 28 + 127 files changed, 6180 insertions(+), 1816 deletions(-) create mode 100644 .devcontainer/config/nvim/init.lua create mode 100644 .devcontainer/config/nvim/lazy-lock.json create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/setup.sh create mode 100644 .luarc.json create mode 100644 CLAUDE.md create mode 100644 doc/development.md create mode 100644 doc/server-capabilities.md create mode 100644 doc/ts-to-lua-guide.md create mode 100644 lua/async/runner.lua create mode 100644 lua/async/waits/wait.lua create mode 100644 lua/async/waits/wait_all.lua create mode 100644 lua/async/waits/wait_with_error_handler.lua create mode 100644 lua/async/wrap.lua create mode 100644 lua/java-core/constants/java_version.lua create mode 100644 lua/java-core/ls/clients/java-debug-client.lua create mode 100644 lua/java-core/ls/clients/java-test-client.lua create mode 100644 lua/java-core/ls/clients/jdtls-client.lua create mode 100644 lua/java-core/ls/servers/jdtls/cmd.lua create mode 100644 lua/java-core/ls/servers/jdtls/conf.lua create mode 100644 lua/java-core/ls/servers/jdtls/env.lua create mode 100644 lua/java-core/ls/servers/jdtls/init.lua create mode 100644 lua/java-core/ls/servers/jdtls/plugins.lua create mode 100644 lua/java-core/ls/servers/jdtls/root.lua create mode 100644 lua/java-core/types/jdtls-types.lua create mode 100644 lua/java-core/types/nvim-types.lua rename lua/{java => java-core}/utils/buffer.lua (100%) create mode 100644 lua/java-core/utils/class.lua create mode 100644 lua/java-core/utils/command.lua rename lua/{java/handlers/error.lua => java-core/utils/error_handler.lua} (78%) create mode 100644 lua/java-core/utils/errors.lua create mode 100644 lua/java-core/utils/event.lua create mode 100644 lua/java-core/utils/file.lua create mode 100644 lua/java-core/utils/list.lua rename lua/{java => java-core}/utils/log.lua (86%) create mode 100644 lua/java-core/utils/log2.lua create mode 100644 lua/java-core/utils/lsp.lua create mode 100644 lua/java-core/utils/notify.lua create mode 100644 lua/java-core/utils/path.lua create mode 100644 lua/java-core/utils/set.lua create mode 100644 lua/java-core/utils/system.lua create mode 100644 lua/java-dap/data-adapters.lua create mode 100644 lua/java-dap/init.lua create mode 100644 lua/java-dap/runner.lua create mode 100644 lua/java-dap/setup.lua create mode 100644 lua/java-refactor/action.lua create mode 100644 lua/java-refactor/api/build.lua create mode 100644 lua/java-refactor/api/refactor.lua create mode 100644 lua/java-refactor/client-command-handlers.lua create mode 100644 lua/java-refactor/client-command.lua create mode 100644 lua/java-refactor/init.lua create mode 100644 lua/java-refactor/refactor.lua create mode 100644 lua/java-refactor/utils/error_handler.lua create mode 100644 lua/java-refactor/utils/instance-factory.lua rename lua/{java/runner => java-runner}/run-logger.lua (100%) rename lua/{java/runner => java-runner}/run.lua (94%) rename lua/{java/runner => java-runner}/runner.lua (84%) create mode 100644 lua/java-test/adapters.lua create mode 100644 lua/java-test/api.lua create mode 100644 lua/java-test/init.lua create mode 100644 lua/java-test/reports/junit.lua create mode 100644 lua/java-test/results/execution-status.lua create mode 100644 lua/java-test/results/message-id.lua create mode 100644 lua/java-test/results/result-parser-factory.lua create mode 100644 lua/java-test/results/result-parser.lua create mode 100644 lua/java-test/results/result-status.lua create mode 100644 lua/java-test/ui/floating-report-viewer.lua create mode 100644 lua/java-test/ui/report-viewer.lua create mode 100644 lua/java-test/utils/string-builder.lua delete mode 100644 lua/java/api/dap.lua delete mode 100644 lua/java/api/test.lua delete mode 100644 lua/java/dap/init.lua delete mode 100644 lua/java/startup/duplicate-setup-check.lua delete mode 100644 lua/java/startup/exec-order-check.lua create mode 100644 lua/java/startup/lsp_setup.lua delete mode 100644 lua/java/startup/lspconfig-setup-wrap.lua delete mode 100644 lua/java/startup/mason-dep.lua delete mode 100644 lua/java/startup/mason-registry-check.lua delete mode 100644 lua/java/startup/nvim-dep.lua delete mode 100644 lua/java/startup/startup-check.lua delete mode 100644 lua/java/treesitter/init.lua delete mode 100644 lua/java/treesitter/queries.lua rename lua/java/{utils/ui.lua => ui/utils.lua} (92%) delete mode 100644 lua/java/utils/command.lua delete mode 100644 lua/java/utils/instance_factory.lua delete mode 100644 lua/java/utils/jdtls.lua delete mode 100644 lua/java/utils/jdtls2.lua delete mode 100644 lua/java/utils/mason.lua create mode 100644 lua/pkgm/downloaders/factory.lua create mode 100644 lua/pkgm/downloaders/powershell.lua create mode 100644 lua/pkgm/downloaders/wget.lua create mode 100644 lua/pkgm/extractors/factory.lua create mode 100644 lua/pkgm/extractors/powershell.lua create mode 100644 lua/pkgm/extractors/tar.lua create mode 100644 lua/pkgm/extractors/uncompressed.lua create mode 100644 lua/pkgm/extractors/unzip.lua create mode 100644 lua/pkgm/manager.lua create mode 100644 lua/pkgm/pkgs/jdtls.lua create mode 100644 lua/pkgm/specs/base-spec.lua create mode 100644 lua/pkgm/specs/init.lua create mode 100644 lua/pkgm/specs/jdtls-spec/init.lua create mode 100644 lua/pkgm/specs/jdtls-spec/version-map.lua create mode 100644 tests/constants/capabilities.lua delete mode 100644 tests/prepare-config.lua create mode 100644 tests/specs/capabilities_spec.lua create mode 100644 tests/specs/jdtls_extensions_spec.lua create mode 100644 tests/specs/lsp_spec.lua delete mode 100644 tests/test-config.lua create mode 100644 tests/utils/lsp-utils.lua create mode 100644 tests/utils/prepare-config.lua create mode 100644 tests/utils/test-config.lua diff --git a/.devcontainer/config/nvim/init.lua b/.devcontainer/config/nvim/init.lua new file mode 100644 index 0000000..56e8214 --- /dev/null +++ b/.devcontainer/config/nvim/init.lua @@ -0,0 +1,126 @@ +local colemak = true + +local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' + +if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + 'git', + 'clone', + '--filter=blob:none', + 'https://github.com/folke/lazy.nvim.git', + '--branch=stable', + lazypath, + }) +end + +vim.opt.rtp:prepend(lazypath) + +-- Setup lazy.nvim +require('lazy').setup({ + { + 'nvim-java/nvim-java', + dir = '/workspaces/nvim-java', + config = function() + require('java').setup() + vim.lsp.config('jdtls', { + handlers = { + ['language/status'] = function(_, data) + vim.notify(data.message, vim.log.levels.INFO) + end, + + ['$/progress'] = function(_, data) + vim.notify(data.value.message, vim.log.levels.INFO) + end, + }, + }) + vim.lsp.enable('jdtls') + end, + }, +}) + +-- Basic settings +vim.g.mapleader = ' ' +vim.opt.number = true +vim.opt.relativenumber = true +vim.opt.tabstop = 2 +vim.opt.shiftwidth = 2 +vim.opt.expandtab = false +vim.opt.completeopt = { 'menu', 'menuone', 'noselect' } + +vim.keymap.set('n', '', 'q') + +if colemak then + vim.keymap.set('n', '', '') + vim.keymap.set('n', 'E', 'K') + vim.keymap.set('n', 'H', 'I') + vim.keymap.set('n', 'K', 'N') + vim.keymap.set('n', 'L', 'E') + vim.keymap.set('n', 'N', 'J') + vim.keymap.set('n', 'e', '') + vim.keymap.set('n', 'h', 'i') + vim.keymap.set('n', 'i', '') + vim.keymap.set('n', 'j', 'm') + vim.keymap.set('n', 'k', 'n') + vim.keymap.set('n', 'l', 'e') + vim.keymap.set('n', 'm', '') + vim.keymap.set('n', 'n', '') +end + +vim.api.nvim_create_autocmd('LspAttach', { + callback = function(args) + vim.lsp.completion.enable(true, args.data.client_id, args.buf, { autotrigger = true }) + vim.keymap.set('i', '', function() + vim.lsp.completion.get() + end, { buffer = args.buf }) + + if colemak then + vim.keymap.set('i', '', '', { buffer = args.buf }) + vim.keymap.set('i', '', '', { buffer = args.buf }) + end + end, +}) + +vim.keymap.set('n', ']d', function() + vim.diagnostic.jump({ count = 1, float = true }) +end, { desc = 'Jump to next diagnostic' }) + +vim.keymap.set('n', '[d', function() + vim.diagnostic.jump({ count = -1, float = true }) +end, { desc = 'Jump to previous diagnostic' }) + +vim.keymap.set('n', 'ta', vim.lsp.buf.code_action, {}) + +-- DAP keymaps +vim.keymap.set('n', 'dd', function() + require('dap').toggle_breakpoint() +end, { desc = 'Toggle breakpoint' }) + +vim.keymap.set('n', 'dc', function() + require('dap').continue() +end, { desc = 'Continue' }) + +vim.keymap.set('n', 'dn', function() + require('dap').step_over() +end, { desc = 'Step over' }) + +vim.keymap.set('n', 'di', function() + require('dap').step_into() +end, { desc = 'Step into' }) + +vim.keymap.set('n', 'do', function() + require('dap').step_out() +end, { desc = 'Step out' }) + +vim.keymap.set('n', 'dr', function() + require('dap').repl.open() +end, { desc = 'Open REPL' }) + +vim.keymap.set('n', 'dl', function() + require('dap').run_last() +end, { desc = 'Run last' }) + +vim.keymap.set('n', 'dt', function() + require('dap').terminate() +end, { desc = 'Terminate' }) + +vim.keymap.set('n', 'm', "vnewput = execute('messages')") diff --git a/.devcontainer/config/nvim/lazy-lock.json b/.devcontainer/config/nvim/lazy-lock.json new file mode 100644 index 0000000..c076196 --- /dev/null +++ b/.devcontainer/config/nvim/lazy-lock.json @@ -0,0 +1,6 @@ +{ + "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, + "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, + "nvim-dap": { "branch": "master", "commit": "b38f7d30366d9169d0a623c4c85fbcf99d8d58bb" }, + "spring-boot.nvim": { "branch": "main", "commit": "218c0c26c14d99feca778e4d13f5ec3e8b1b60f0" } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8fe9ee8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.base.schema.json", + "name": "nvim-java", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/java:1": {}, + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/devcontainers-extra/features/wget-apt-get:1": {}, + "ghcr.io/duduribeiro/devcontainer-features/neovim:1": { + "version": "nightly" + }, + "ghcr.io/devcontainers-extra/features/springboot-sdkman:2": {} + }, + "postCreateCommand": "bash .devcontainer/setup.sh", + "customizations": { + "vscode": { + "extensions": ["sumneko.lua"] + } + } +} diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 0000000..843e7c7 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail + +mkdir -p ~/.config +ln -sf /workspaces/nvim-java/.devcontainer/config/nvim ~/.config/nvim diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d46b2ff..9875bb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,12 +8,13 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, macos-latest, windows-latest] nvim-versions: ["stable", "nightly"] - name: test + name: test (${{ matrix.os }}, nvim-${{ matrix.nvim-versions }}) steps: - name: checkout uses: actions/checkout@v3 @@ -24,4 +25,4 @@ jobs: version: ${{ matrix.nvim-versions }} - name: run tests - run: make test + run: make tests diff --git a/.gitignore b/.gitignore index f2a462f..f8b34a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ vendor/plenary.nvim .test_plugins -.luarc.json +proj diff --git a/.luacheckrc b/.luacheckrc index 2998a81..33c63e5 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -12,3 +12,6 @@ read_globals = { 'it', 'assert', } +ignore = { + '212/self', +} diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..acd7d9d --- /dev/null +++ b/.luarc.json @@ -0,0 +1,4 @@ +{ + "diagnostics.globals": ["describe"] +} + diff --git a/.stylua.toml b/.stylua.toml index 31321df..dc96236 100644 --- a/.stylua.toml +++ b/.stylua.toml @@ -1,4 +1,4 @@ -column_width = 80 +column_width = 120 line_endings = "Unix" indent_type = "Tabs" indent_width = 2 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index de6d140..e951a20 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,9 +1,10 @@ { - "tasks": [ - { - "label": "Run Tests", - "type": "shell", - "command": "make test" - } - ] + "tasks": [ + { + "label": "run current test file", + "type": "shell", + "command": "make test FILE=${file}", + "problemMatcher": [] + } + ] } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d716f17 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,153 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## General Guidelines + +- Be extremely concise in all interactions and commit messages +- Sacrifice grammar for sake of concision +- Keep CLAUDE.md updated when changes make it outdated; add noteworthy patterns/conventions after implementing changes + +## Documentation + +Additional documentation available in `/doc`: +- `development.md` - Development environment setup (devcontainer, Spring Boot test projects) +- `nvim-java.txt` - Plugin documentation +- `server-capabilities.md` - Server capabilities reference +- `ts-to-lua-guide.md` - TypeScript to Lua translation guide + +## Build Commands + +```bash +make tests # Run Plenary/Busted tests (headless Neovim) +make test FILE=path # Run specific test file +make lint # Run luacheck linter +make format # Format with stylua +make all # lint -> format -> tests +``` + +## Architecture + +nvim-java is a Neovim plugin providing Java IDE features via JDTLS wrapper. Monorepo structure with bundled submodules: + +``` +lua/ +├── java.lua # Main API entry point +├── java/ # Core plugin (startup, config, api/, runner/, ui/, utils/) +├── java-core/ # LSP integration (ls/servers/jdtls/, clients/, adapters/, utils/) +├── java-test/ # Test runner (ui/, results/, reports/) +├── java-dap/ # Debug adapter (api/, data/) +├── java-refactor/ # Refactoring tools (api/, client-commands/) +├── pkgm/ # Package management (downloaders/, extractors/, version control) +└── async/ # Custom async/await wrapper +plugin/java.lua # User command registration +``` + +**java-core Details:** +- Core JDTLS features implementation +- `ls/servers/jdtls/` - JDTLS config generator, loads extensions (java-test, java-debug, spring-boot, lombok). Uses mason.nvim APIs for extension paths +- `ls/clients/` - LSP request wrappers: + - `jdtls-client.lua` - Core JDTLS LSP calls + - `java-test-client.lua`, `java-debug-client.lua` - Extension-specific calls + - Purpose: wrap async APIs with coroutines for sync-style calls, add typing + - 1:1 mappings of VSCode projects for Neovim: + - vscode-java, vscode-java-test, vscode-java-debug + +**pkgm Details:** +- Package management utilities +- `downloaders/` - Download implementations (wget, etc.) +- `extractors/` - Archive extraction utilities +- Version control and package lifecycle management +- `specs/jdtls-spec/version-map.lua` - JDTLS version to timestamp mapping + +**Updating JDTLS Versions:** +1. Visit https://download.eclipse.org/jdtls/milestones/ +2. Click version link in 'Directory Contents' section +3. Find file: `jdt-language-server-X.Y.Z-YYYYMMDDHHSS.tar.gz` +4. Extract version (X.Y.Z) and timestamp (YYYYMMDDHHSS) +5. Add to `version-map.lua`: `['X.Y.Z'] = 'YYYYMMDDHHSS'` + +**Key Files:** +- `lua/java/config.lua` - Default configuration (JDTLS version, plugins, JDK) +- `lua/java-core/ls/servers/jdtls/config.lua` - JDTLS server configuration +- `lazy.lua` - lazy.nvim plugin spec with dependencies + +## Test Structure + +``` +tests/ +├── assets/ # Test fixtures and assets (e.g., HelloWorld.java) +├── constants/ # Test constants (e.g., capabilities.lua) +├── utils/ # Test utilities and config files +│ ├── lsp-utils.lua # LSP test helpers +│ ├── prepare-config.lua # Lazy.nvim test setup +│ └── test-config.lua # Manual test setup +└── specs/ # Test specifications + ├── lsp_spec.lua # All LSP-related tests + └── pkgm_spec.lua # All pkgm-related tests +``` + +**Test Guidelines:** +- Group related tests in single spec file (e.g., all pkgm tests in `pkgm_spec.lua`) +- Extract reusable logic to `utils/` to keep test steps clean +- Store test data/fixtures in `assets/` +- Store constants (capabilities, expected values) in `constants/` + +## Code Patterns + +**Event-driven registration:** Modules register on JDTLS attach via `event.on_jdtls_attach()` + +**Config merging:** `vim.tbl_deep_extend('force', global_config, user_config or {})` + +**Config type sync:** When modifying `lua/java/config.lua` (add/update/delete properties), update both `java.Config` type and `java.PartialConfig` in `lua/java.lua` to keep types in sync + +**Complex types:** If type contains complex object, create class type instead of inlining type everywhere + +**Async operations:** Uses custom async/await in `lua/async/` instead of raw coroutines + +**User commands:** Registered in `plugin/java.lua`, map to nested API in `lua/java/api/` + +**Class creation:** Use `java-core/utils/class` (Penlight class system): +```lua +local class = require('java-core.utils.class') + +local Base = class() +function Base:_init(name) + self.name = name +end + +local Child = class(Base) +function Child:_init(name, age) + self:super(name) + self.age = age +end +``` + +**Logging:** Use `java-core/utils/log2` for all logging: +```lua +local log = require('java-core.utils.log2') +log.trace('trace message') +log.debug('debug message') +log.info('info message') +log.warn('warning message') +log.error('error message') +log.fatal('fatal message') +``` + +## Code Style + +- Tabs for indentation (tab width: 2) +- Line width: 80 chars +- Single quotes preferred +- LuaDoc annotations (`---@`) for types +- Type naming: Prefix types with package name (e.g., `java.Config`, `java-debug.DebugConfig`) +- Neovim globals allowed: vim.o, vim.g, vim.wo, vim.bo, vim.opt, vim.lsp +- Private methods: Use `@private` annotation, NOT `_` prefix (except `_init` constructor) +- Method syntax: Always use `:` for class methods regardless of `self` usage + - All class methods: `function ClassName:method_name()` (with or without `self`) + +## Git Guidelines + +- Use conventional commit messages per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +- Never append "generated by AI" message +- Split unrelated changes into separate commits diff --git a/Makefile b/Makefile index 2a8f9d4..d64073d 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,26 @@ SRC_DIR=lua -TESTS_DIR=tests -PREPARE_CONFIG=${TESTS_DIR}/prepare-config.lua -TEST_CONFIG=${TESTS_DIR}/test-config.lua +TESTS_ROOT=tests +TESTS_DIR?=${TESTS_ROOT}/specs +PREPARE_CONFIG=${TESTS_ROOT}/utils/prepare-config.lua +TEST_CONFIG=${TESTS_ROOT}/utils/test-config.lua +TEST_TIMEOUT?=60000 -.PHONY: test lint format all +.PHONY: test tests lint format all -all: lint format test +all: lint format tests + +tests: + @nvim \ + --headless \ + -u ${PREPARE_CONFIG} \ + "+PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TEST_CONFIG}', timeout = ${TEST_TIMEOUT}, sequential = true }" test: @nvim \ --headless \ -u ${PREPARE_CONFIG} \ - "+PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TEST_CONFIG}' }" + "+PlenaryBustedDirectory ${FILE} { minimal_init = '${TEST_CONFIG}', timeout = ${TEST_TIMEOUT} }" + lint: luacheck ${SRC_DIR} ${TESTS_DIR} diff --git a/README.md b/README.md index c540b14..16d1022 100644 --- a/README.md +++ b/README.md @@ -7,500 +7,4 @@ ![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white) ![Lua](https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white) -Just install and start writing `public static void main(String[] args)`. - -> [!CAUTION] -> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` before installing this - -> [!TIP] -> You can find cool tips & tricks here https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks - -> [!NOTE] -> If you are facing errors while using, please check troubleshoot wiki https://github.com/nvim-java/nvim-java/wiki/Troubleshooting - -## :loudspeaker: Demo - - - -## :dizzy: Features - -- :white_check_mark: Spring Boot Tools -- :white_check_mark: Diagnostics & Auto Completion -- :white_check_mark: Automatic Debug Configuration -- :white_check_mark: Organize Imports & Code Formatting -- :white_check_mark: Running Tests -- :white_check_mark: Run & Debug Profiles -- :white_check_mark: [Code Actions](https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks#running-code-actions) - -## :bulb: Why - -- Everything necessary will be installed automatically -- Uses [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) to setup `jdtls` -- Realtime server settings updates is possible using [neoconf](https://github.com/folke/neoconf.nvim) -- Auto loads necessary `jdtls` plugins - - Supported plugins are, - - `spring-boot-tools` - - `lombok` - - `java-test` - - `java-debug-adapter` - -## :hammer: How to Install - -
- -:small_orange_diamond:details - -### Starter Configs (Recommend for newbies) - -Following are forks of original repositories pre-configured for java. If you -don't know how to get started, use one of the following to get started. -You can click on **n commits ahead of** link to see the changes made on top of the original project - -- [LazyVim](https://github.com/nvim-java/starter-lazyvim) -- [Kickstart](https://github.com/nvim-java/starter-kickstart) -- [AstroNvim](https://github.com/nvim-java/starter-astronvim) - -### Custom Configuration Instructions - -- Install the plugin - -Using [lazy.nvim](https://github.com/folke/lazy.nvim) - -```lua -return {'nvim-java/nvim-java'} -``` - -- Setup nvim-java before `lspconfig` - -```lua -require('java').setup() -``` - -- Setup jdtls like you would usually do - -```lua -require('lspconfig').jdtls.setup({}) -``` - -Yep! That's all :) - -
- -## :keyboard: Commands - -
- -:small_orange_diamond:details - -### Build - -- `JavaBuildBuildWorkspace` - Runs a full workspace build - -- `JavaBuildCleanWorkspace` - Clear the workspace cache - (for now you have to close and reopen to restart the language server after - the deletion) - -### Runner - -- `JavaRunnerRunMain` - Runs the application or selected main class (if there - are multiple main classes) - -```vim -:JavaRunnerRunMain -:JavaRunnerRunMain -``` - -- `JavaRunnerStopMain` - Stops the running application -- `JavaRunnerToggleLogs` - Toggle between show & hide runner log window - -### DAP - -- `JavaDapConfig` - DAP is autoconfigured on start up, but in case you want to - force configure it again, you can use this API - -### Test - -- `JavaTestRunCurrentClass` - Run the test class in the active buffer -- `JavaTestDebugCurrentClass` - Debug the test class in the active buffer -- `JavaTestRunCurrentMethod` - Run the test method on the cursor -- `JavaTestDebugCurrentMethod` - Debug the test method on the cursor -- `JavaTestViewLastReport` - Open the last test report in a popup window - -### Profiles - -- `JavaProfile` - Opens the profiles UI - -### Refactor - -- `JavaRefactorExtractVariable` - Create a variable from value at cursor/selection -- `JavaRefactorExtractVariableAllOccurrence` - Create a variable for all - occurrences from value at cursor/selection -- `JavaRefactorExtractConstant` - Create a constant from the value at cursor/selection -- `JavaRefactorExtractMethod` - Create a method from the value at cursor/selection -- `JavaRefactorExtractField` - Create a field from the value at cursor/selection - -### Settings - -- `JavaSettingsChangeRuntime` - Change the JDK version to another - -
- -## :computer: APIs - -
- -:small_orange_diamond:details - -### Build - -- `build.build_workspace` - Runs a full workspace build - -```lua -require('java').build.build_workspace() -``` - -- `build.clean_workspace` - Clear the workspace cache - (for now you have to close and reopen to restart the language server after - the deletion) - -```lua -require('java').build.clean_workspace() -``` - -### Runner - -- `built_in.run_app` - Runs the application or selected main class (if there - are multiple main classes) - -```lua -require('java').runner.built_in.run_app({}) -require('java').runner.built_in.run_app({'arguments', 'to', 'pass', 'to', 'main'}) -``` - -- `built_in.stop_app` - Stops the running application - -```lua -require('java').runner.built_in.stop_app() -``` - -- `built_in.toggle_logs` - Toggle between show & hide runner log window - -```lua -require('java').runner.built_in.toggle_logs() -``` - -### DAP - -- `config_dap` - DAP is autoconfigured on start up, but in case you want to force - configure it again, you can use this API - -```lua -require('java').dap.config_dap() -``` - -### Test - -- `run_current_class` - Run the test class in the active buffer - -```lua -require('java').test.run_current_class() -``` - -- `debug_current_class` - Debug the test class in the active buffer - -```lua -require('java').test.debug_current_class() -``` - -- `run_current_method` - Run the test method on the cursor - -```lua -require('java').test.run_current_method() -``` - -- `debug_current_method` - Debug the test method on the cursor - -```lua -require('java').test.debug_current_method() -``` - -- `view_report` - Open the last test report in a popup window - -```lua -require('java').test.view_last_report() -``` - -### Profiles - -```lua -require('java').profile.ui() -``` - -### Refactor - -- `extract_variable` - Create a variable from value at cursor/selection - -```lua -require('java').refactor.extract_variable() -``` - -- `extract_variable_all_occurrence` - Create a variable for all occurrences from - value at cursor/selection - -```lua -require('java').refactor.extract_variable_all_occurrence() -``` - -- `extract_constant` - Create a constant from the value at cursor/selection - -```lua -require('java').refactor.extract_constant() -``` - -- `extract_method` - Create method from the value at cursor/selection - -```lua -require('java').refactor.extract_method() -``` - -- `extract_field` - Create a field from the value at cursor/selection - -```lua -require('java').refactor.extract_field() -``` - -### Settings - -- `change_runtime` - Change the JDK version to another - -```lua -require('java').settings.change_runtime() -``` - -
- -## :clamp: How to Use JDK X.X Version? - -
- -:small_orange_diamond:details - -### Method 1 - -[Neoconf](https://github.com/folke/neoconf.nvim) can be used to manage LSP -setting including jdtls. Neoconf allows global configuration as well as project-wise -configurations. Here is how you can set Jdtls setting on `neoconf.json` - -```json -{ - "lspconfig": { - "jdtls": { - "java.configuration.runtimes": [ - { - "name": "JavaSE-21", - "path": "/opt/jdk-21", - "default": true - } - ] - } - } -} -``` - -### Method 2 - -Pass the settings to Jdtls setup. - -```lua -require('lspconfig').jdtls.setup({ - settings = { - java = { - configuration = { - runtimes = { - { - name = "JavaSE-21", - path = "/opt/jdk-21", - default = true, - } - } - } - } - } -}) -``` - -
- -## :wrench: Configuration - -
- -:small_orange_diamond:details - -For most users changing the default configuration is not necessary. But if you -want, following options are available - -```lua -{ - -- list of file that exists in root of the project - root_markers = { - 'settings.gradle', - 'settings.gradle.kts', - 'pom.xml', - 'build.gradle', - 'mvnw', - 'gradlew', - 'build.gradle', - 'build.gradle.kts', - '.git', - }, - - jdtls = { - version = 'v1.43.0', - }, - - lombok = { - version = 'nightly', - }, - - -- load java test plugins - java_test = { - enable = true, - version = '0.40.1', - }, - - -- load java debugger plugins - java_debug_adapter = { - enable = true, - version = '0.58.1', - }, - - spring_boot_tools = { - enable = true, - version = '1.55.1', - }, - - jdk = { - -- install jdk using mason.nvim - auto_install = true, - version = '17.0.2', - }, - - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, - }, - - -- We do multiple verifications to make sure things are in place to run this - -- plugin - verification = { - -- nvim-java checks for the order of execution of following - -- * require('java').setup() - -- * require('lspconfig').jdtls.setup() - -- IF they are not executed in the correct order, you will see a error - -- notification. - -- Set following to false to disable the notification if you know what you - -- are doing - invalid_order = true, - - -- nvim-java checks if the require('java').setup() is called multiple - -- times. - -- IF there are multiple setup calls are executed, an error will be shown - -- Set following property value to false to disable the notification if - -- you know what you are doing - duplicate_setup_calls = true, - - -- nvim-java checks if nvim-java/mason-registry is added correctly to - -- mason.nvim plugin. - -- IF it's not registered correctly, an error will be thrown and nvim-java - -- will stop setup - invalid_mason_registry = false, - }, - - mason = { - -- These mason registries will be prepended to the existing mason - -- configuration - registries = { - 'github:nvim-java/mason-registry', - }, - }, -} - -``` - -
- -## :golf: Architecture - -
- -:small_orange_diamond:details - -Following is the high level idea. Jdtls is the language server nvim-java -communicates with. However, we don't have all the features we need just in -Jdtls. So, we are loading java-test & java-debug-adapter extensions when we -launch Jdtls. Once the language server is started, we communicate with the -language server to do stuff. - -For instance, to run the current test, - -- Request Jdtls for test classes -- Request Jdtls for class paths, module paths, java executable -- Request Jdtls to start a debug session and send the port of the session back -- Prepare TCP connections to listen to the test results -- Start nvim-dap and let user interactions to be handled by nvim-dap -- Parse the test results as they come in -- Once the execution is done, open a window show the test results - -```text - ┌────────────┐ ┌────────────┐ - │ │ │ │ - │ Neovim │ │ VSCode │ - │ │ │ │ - └─────▲──────┘ └──────▲─────┘ - │ │ - │ │ - │ │ - │ │ -┌───────▼───────┐ ┌──────────────▼──────────────┐ -│ │ │ │ -│ nvim-java │ │ Extension Pack for Java │ -│ │ │ │ -└───────▲───────┘ └──────────────▲──────────────┘ - │ │ - │ │ - │ │ - │ │ - │ │ - │ ┌───────────┐ │ - │ │ │ │ - └──────────────► JDTLS ◄────────────┘ - │ │ - └───▲───▲───┘ - │ │ - │ │ - │ │ - │ │ - │ │ - ┌───────────────┐ │ │ ┌────────────────────────┐ - │ │ │ │ │ │ - │ java-test ◄────────┘ └─────────► java-debug-adapter │ - │ │ │ │ - └───────────────┘ └────────────────────────┘ -``` - -
- -## :bookmark_tabs: Projects Acknowledgement - -- [spring-boot.nvim](https://github.com/JavaHello/spring-boot.nvim) is the one - that starts sts4 & do other necessary `jdtls` `sts4` sync command registration - in `nvim-java`. - -- [nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls) is a plugin that follows - "Keep it simple, stupid!" approach. If you love customizing things by yourself, - then give nvim-jdtls a try. - -> [!WARNING] -> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` -> before installing this +monorepo is a complete rewrite. Do not use this plugin. This will be released as nvim-java 4.0.0 in the original repo soon. diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 0000000..60ac94b --- /dev/null +++ b/doc/development.md @@ -0,0 +1,51 @@ +# Development Environment Setup + +## Prerequisites + +- Docker +- devcontainer CLI (`npm install -g @devcontainers/cli`) + +## Getting Started + +Build and start devcontainer: + +```bash +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +The devcontainer includes: +- Java (via devcontainer feature) +- Neovim nightly +- Python +- Spring Boot CLI (via SDKMAN) +- wget + +Neovim config auto-links from `.devcontainer/config/nvim` to `~/.config/nvim` + +## Build Commands + +```bash +make tests # Run Plenary/Busted tests (headless Neovim) +make test FILE=path # Run specific test file +make lint # Run luacheck linter +make format # Format with stylua +make all # lint -> format -> tests +``` + +## Creating Test Projects + +### Spring Boot Project + +Create Spring Boot project inside devcontainer: + +```bash +spring init -d web,lombok --extract demo +``` + +This creates `demo/` with Web and Lombok dependencies. + +Options: +- `-d` dependencies (comma-separated) +- `--extract` extract to directory (default: creates zip) +- See `spring help init` for more options diff --git a/doc/server-capabilities.md b/doc/server-capabilities.md new file mode 100644 index 0000000..688e699 --- /dev/null +++ b/doc/server-capabilities.md @@ -0,0 +1,164 @@ +```lua +{ + callHierarchyProvider = true, + codeActionProvider = { + resolveProvider = true + }, + codeLensProvider = { + resolveProvider = true + }, + completionProvider = { + completionItem = { + labelDetailsSupport = true + }, + resolveProvider = true, + triggerCharacters = { ".", "@", "#", "*", " " } + }, + definitionProvider = true, + documentFormattingProvider = true, + documentHighlightProvider = true, + documentOnTypeFormattingProvider = { + firstTriggerCharacter = ";", + moreTriggerCharacter = { "\n", "}" } + }, + documentRangeFormattingProvider = true, + documentSymbolProvider = true, + executeCommandProvider = { + commands = { + "java.completion.onDidSelect", + "java.decompile", + "java.edit.handlePasteEvent", + "java.edit.organizeImports", + "java.edit.smartSemicolonDetection", + "java.edit.stringFormatting", + "java.navigate.openTypeHierarchy", + "java.navigate.resolveTypeHierarchy", + "java.project.addToSourcePath", + "java.project.createModuleInfo", + "java.project.getAll", + "java.project.getClasspaths", + "java.project.getSettings", + "java.project.import", + "java.project.isTestFile", + "java.project.listSourcePaths", + "java.project.refreshDiagnostics", + "java.project.removeFromSourcePath", + "java.project.resolveSourceAttachment", + "java.project.resolveStackTraceLocation", + "java.project.resolveWorkspaceSymbol", + "java.project.updateSourceAttachment", + "java.project.upgradeGradle", + "java.protobuf.generateSources", + "java.reloadBundles", + "vscode.java.buildWorkspace", + "vscode.java.checkProjectSettings", + "vscode.java.fetchPlatformSettings", + "vscode.java.fetchUsageData", + "vscode.java.inferLaunchCommandLength", + "vscode.java.isOnClasspath", + "vscode.java.resolveBuildFiles", + "vscode.java.resolveClassFilters", + "vscode.java.resolveClasspath", + "vscode.java.resolveElementAtSelection", + "vscode.java.resolveInlineVariables", + "vscode.java.resolveJavaExecutable", + "vscode.java.resolveMainClass", + "vscode.java.resolveMainMethod", + "vscode.java.resolveSourceUri", + "vscode.java.startDebugSession", + "vscode.java.test.findDirectTestChildrenForClass", + "vscode.java.test.findJavaProjects", + "vscode.java.test.findTestLocation", + "vscode.java.test.findTestPackagesAndTypes", + "vscode.java.test.findTestTypesAndMethods", + "vscode.java.test.generateTests", + "vscode.java.test.get.testpath", + "vscode.java.test.junit.argument", + "vscode.java.test.navigateToTestOrTarget", + "vscode.java.test.resolvePath", + "vscode.java.updateDebugSettings", + "vscode.java.validateLaunchConfig", + } + }, + foldingRangeProvider = true, + hoverProvider = true, + implementationProvider = true, + inlayHintProvider = true, + referencesProvider = true, + renameProvider = { + prepareProvider = true + }, + selectionRangeProvider = true, + semanticTokensProvider = { + documentSelector = { { + language = "java", + scheme = "file" + }, { + language = "java", + scheme = "jdt" + } }, + full = { + delta = false + }, + legend = { + tokenModifiers = { + "abstract", + "constructor", + "declaration", + "deprecated", + "documentation", + "generic", + "importDeclaration", + "native", + "private", + "protected", + "public", + "readonly", + "static", + "typeArgument", + }, + tokenTypes = { + "annotation", + "annotationMember", + "class", + "enum", + "enumMember", + "interface", + "keyword", + "method", + "modifier", + "namespace", + "parameter", + "property", + "record", + "recordComponent", + "type", + "typeParameter", + "variable", + } + }, + range = false + }, + signatureHelpProvider = { + triggerCharacters = { "(", "," } + }, + textDocumentSync = { + change = 2, + openClose = true, + save = { + includeText = true + }, + willSave = true, + willSaveWaitUntil = true + }, + typeDefinitionProvider = true, + typeHierarchyProvider = true, + workspace = { + workspaceFolders = { + changeNotifications = true, + supported = true + } + }, + workspaceSymbolProvider = true +} +``` diff --git a/doc/ts-to-lua-guide.md b/doc/ts-to-lua-guide.md new file mode 100644 index 0000000..cf46257 --- /dev/null +++ b/doc/ts-to-lua-guide.md @@ -0,0 +1,35 @@ +# Personal Notes + +## Communication + +We are using `request` function of `vim.lsp.Client` function to communicate with +the `jdtls`. + +```lua +fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer?): boolean, integer?`) +``` + +This has almost 1 to 1 mapping with `vscode` APIs most of the time. + +```typescript +await this.languageClient.sendRequest( + method: string, + params: any, + // handler is not passed since there is async / await + // buffer I'm guessing is set to current buffer by default??? +); +``` + +However, some APIs sends more arguments, to which we don't have a Neovim lua +equivalent I'm guessing. Following is an example. + +```typescript +await this.languageClient.sendRequest( + CompileWorkspaceRequest.type, + isFullCompile, + token, +); +``` + +To make this request, probably `client.rpc.request` should be used without +`request()` wrapper. diff --git a/lazy.lua b/lazy.lua index 03ef45a..5e21aec 100644 --- a/lazy.lua +++ b/lazy.lua @@ -1,26 +1,11 @@ return { 'nvim-java/nvim-java', dependencies = { - 'nvim-java/lua-async-await', - 'nvim-java/nvim-java-refactor', - 'nvim-java/nvim-java-core', - 'nvim-java/nvim-java-test', - 'nvim-java/nvim-java-dap', 'MunifTanjim/nui.nvim', - 'neovim/nvim-lspconfig', 'mfussenegger/nvim-dap', { 'JavaHello/spring-boot.nvim', commit = '218c0c26c14d99feca778e4d13f5ec3e8b1b60f0', }, - { - 'mason-org/mason.nvim', - -- opts = { - -- registries = { - -- 'github:nvim-java/mason-registry', - -- 'github:mason-org/mason-registry', - -- }, - -- }, - }, }, } diff --git a/lua/async/runner.lua b/lua/async/runner.lua new file mode 100644 index 0000000..042e1ee --- /dev/null +++ b/lua/async/runner.lua @@ -0,0 +1,63 @@ +local wrap = require('async.wrap') +local co = coroutine + +---Runs the given function +---@param func fun(): any +---@return { run: function, catch: fun(error_handler: fun(error: any)) } +local function runner(func) + local m = { + error_handler = nil, + } + + local async_thunk_factory = wrap(function(handler, parent_handler_callback) + assert(type(handler) == 'function', 'type error :: expected func') + local thread = co.create(handler) + local step = nil + + step = function(...) + local ok, thunk = co.resume(thread, ...) + + -- when an error() is thrown after co-routine is resumed, obviously further + -- processing stops, and resume returns ok(false) and thunk(error) returns + -- the error message + if not ok then + if m.error_handler then + m.error_handler(thunk) + return + end + + if parent_handler_callback then + parent_handler_callback(thunk) + return + end + + error('unhandled error ' .. thunk) + end + + assert(ok, thunk) + if co.status(thread) == 'dead' then + if parent_handler_callback then + parent_handler_callback(thunk) + end + else + assert(type(thunk) == 'function', 'type error :: expected func') + thunk(step) + end + end + + step() + + return m + end) + + m.run = async_thunk_factory(func) + + m.catch = function(error_handler) + m.error_handler = error_handler + return m + end + + return m +end + +return runner diff --git a/lua/async/waits/wait.lua b/lua/async/waits/wait.lua new file mode 100644 index 0000000..b3a4e88 --- /dev/null +++ b/lua/async/waits/wait.lua @@ -0,0 +1,12 @@ +local co = coroutine + +---Waits for async function to be completed +---@generic T +---@param defer fun(callback: fun(result: T)) +---@return T +local function wait(defer) + assert(type(defer) == 'function', 'type error :: expected func') + return co.yield(defer) +end + +return wait diff --git a/lua/async/waits/wait_all.lua b/lua/async/waits/wait_all.lua new file mode 100644 index 0000000..780ce06 --- /dev/null +++ b/lua/async/waits/wait_all.lua @@ -0,0 +1,35 @@ +local co = coroutine + +local function __join(thunks) + local len = #thunks + local done = 0 + local acc = {} + + local thunk = function(step) + if len == 0 then + return step() + end + for i, tk in ipairs(thunks) do + assert(type(tk) == 'function', 'thunk must be function') + local callback = function(...) + acc[i] = { ... } + done = done + 1 + if done == len then + step(unpack(acc)) + end + end + tk(callback) + end + end + return thunk +end + +---Waits for list of async calls to be completed +---@param defer fun(callback: fun(result: any)) +---@return any[] +local function wait_all(defer) + assert(type(defer) == 'table', 'type error :: expected table') + return co.yield(__join(defer)) +end + +return wait_all diff --git a/lua/async/waits/wait_with_error_handler.lua b/lua/async/waits/wait_with_error_handler.lua new file mode 100644 index 0000000..b6e8e43 --- /dev/null +++ b/lua/async/waits/wait_with_error_handler.lua @@ -0,0 +1,20 @@ +local co = coroutine + +---Waits for async function to be completed but considers first parameter as +---error +---@generic T +---@param defer fun(callback: fun(result: T)) +---@return T +local function wait(defer) + assert(type(defer) == 'function', 'type error :: expected func') + + local err, value = co.yield(defer) + + if err then + error(err) + end + + return value +end + +return wait diff --git a/lua/async/wrap.lua b/lua/async/wrap.lua new file mode 100644 index 0000000..f083db3 --- /dev/null +++ b/lua/async/wrap.lua @@ -0,0 +1,20 @@ +---Make the given function a promise automatically assuming the callback +---function is the last argument +---@generic T +---@param func fun(..., callback: fun(result: T)): any +---@return fun(...): T +local function wrap(func) + assert(type(func) == 'function', 'type error :: expected func') + + local factory = function(...) + local params = { ... } + local thunk = function(step) + table.insert(params, step) + return func(unpack(params)) + end + return thunk + end + return factory +end + +return wrap diff --git a/lua/java-core/constants/java_version.lua b/lua/java-core/constants/java_version.lua new file mode 100644 index 0000000..4dc8c05 --- /dev/null +++ b/lua/java-core/constants/java_version.lua @@ -0,0 +1,14 @@ +return { + ['1.43.0'] = { from = 17, to = 21 }, + ['1.44.0'] = { from = 17, to = 21 }, + ['1.45.0'] = { from = 21, to = 21 }, + ['1.46.0'] = { from = 21, to = 21 }, + ['1.46.1'] = { from = 21, to = 21 }, + ['1.47.0'] = { from = 21, to = 21 }, + ['1.48.0'] = { from = 21, to = 21 }, + ['1.49.0'] = { from = 21, to = 21 }, + ['1.50.0'] = { from = 21, to = 21 }, + ['1.51.0'] = { from = 21, to = 21 }, + ['1.52.0'] = { from = 21, to = 21 }, + ['1.53.0'] = { from = 21, to = 21 }, +} diff --git a/lua/java-core/ls/clients/java-debug-client.lua b/lua/java-core/ls/clients/java-debug-client.lua new file mode 100644 index 0000000..d3248ff --- /dev/null +++ b/lua/java-core/ls/clients/java-debug-client.lua @@ -0,0 +1,88 @@ +local class = require('java-core.utils.class') + +local JdtlsClient = require('java-core.ls.clients.jdtls-client') + +---@class java-core.DebugClient: java-core.JdtlsClient +local DebugClient = class(JdtlsClient) + +---@class java-dap.JavaDebugResolveMainClassRecord +---@field mainClass string +---@field projectName string +---@field fileName string + +---Returns a list of main classes in the current workspace +---@return java-dap.JavaDebugResolveMainClassRecord[] # resolved main class +function DebugClient:resolve_main_class() + --- @type java-dap.JavaDebugResolveMainClassRecord[] + return self:workspace_execute_command('vscode.java.resolveMainClass') +end + +---Returns module paths and class paths of a given main class +---@param project_name string +---@param main_class string +---@return string[][] # resolved class and module paths +function DebugClient:resolve_classpath(main_class, project_name) + ---@type string[][] + return self:workspace_execute_command('vscode.java.resolveClasspath', { main_class, project_name }) +end + +---Returns the path to java executable for a given main class +---@param project_name string +---@param main_class string +---@return string # path to java executable +function DebugClient:resolve_java_executable(main_class, project_name) + ---@type string + return self:workspace_execute_command('vscode.java.resolveJavaExecutable', { + main_class, + project_name, + }) +end + +---Returns true if the project settings is the expected +---@param project_name string +---@param main_class string +---@param inheritedOptions boolean +---@param expectedOptions { [string]: any } +---@return boolean # true if the setting is the expected setting +function DebugClient:check_project_settings(main_class, project_name, inheritedOptions, expectedOptions) + ---@type boolean + return self:workspace_execute_command( + 'vscode.java.checkProjectSettings', + ---@diagnostic disable-next-line: param-type-mismatch + vim.fn.json_encode({ + className = main_class, + projectName = project_name, + inheritedOptions = inheritedOptions, + expectedOptions = expectedOptions, + }) + ) +end + +---Starts a debug session and returns the port number +---@return integer # port number of the debug session +function DebugClient:start_debug_session() + ---@type integer + return self:workspace_execute_command('vscode.java.startDebugSession') +end + +---Build the workspace +---@param main_class string +---@param project_name? string +---@param file_path? string +---@param is_full_build boolean +---@return java-core.CompileWorkspaceStatus # compiled status +function DebugClient:build_workspace(main_class, project_name, file_path, is_full_build) + ---@type java-core.CompileWorkspaceStatus + return self:workspace_execute_command( + 'vscode.java.buildWorkspace', + ---@diagnostic disable-next-line: param-type-mismatch + vim.fn.json_encode({ + mainClass = main_class, + projectName = project_name, + filePath = file_path, + isFullBuild = is_full_build, + }) + ) +end + +return DebugClient diff --git a/lua/java-core/ls/clients/java-test-client.lua b/lua/java-core/ls/clients/java-test-client.lua new file mode 100644 index 0000000..ceefee8 --- /dev/null +++ b/lua/java-core/ls/clients/java-test-client.lua @@ -0,0 +1,112 @@ +local log = require('java-core.utils.log2') +local class = require('java-core.utils.class') + +local JdtlsClient = require('java-core.ls.clients.jdtls-client') + +---@class java-core.TestDetails +---@field fullName string +---@field id string +---@field jdtHandler string +---@field label string +---@field projectName string +---@field testKind integer +---@field testLevel integer +---@field uri string + +---@class java-core.TestDetailsWithRange: java-core.TestDetails +---@field range lsp.Range + +---@class java-core.TestDetailsWithChildren: java-core.TestDetails +---@field children java-core.TestDetailsWithRange[] + +---@class java-core.TestDetailsWithChildrenAndRange: java-core.TestDetails +---@field range lsp.Range +---@field children java-core.TestDetailsWithRange[] + +---@class java-core.TestClient: java-core.JdtlsClient +local TestClient = class(JdtlsClient) + +---Returns a list of project details in the current root +---@return java-core.TestDetails[] # test details of the projects +function TestClient:find_java_projects() + ---@type java-core.TestDetails[] + return self:workspace_execute_command( + 'vscode.java.test.findJavaProjects', + { vim.uri_from_fname(self.client.config.root_dir) } + ) +end + +---Returns a list of test package details +---@param handler string +---@param token? string +---@return java-core.TestDetailsWithChildren[] # test package details +function TestClient:find_test_packages_and_types(handler, token) + ---@type java-core.TestDetailsWithChildren[] + return self:workspace_execute_command('vscode.java.test.findTestPackagesAndTypes', { handler, token }) +end + +---Returns test informations in a given file +---@param file_uri string +---@param token? string +---@return java-core.TestDetailsWithChildrenAndRange[] # test details +function TestClient:find_test_types_and_methods(file_uri, token) + ---@type java-core.TestDetailsWithChildrenAndRange[] + return self:workspace_execute_command('vscode.java.test.findTestTypesAndMethods', { file_uri, token }) +end + +---@class java-core.JavaCoreTestJunitLaunchArguments +---@field classpath string[] +---@field mainClass string +---@field modulepath string[] +---@field programArguments string[] +---@field projectName string +---@field vmArguments string[] +---@field workingDirectory string + +---@class java-core.JavaCoreTestResolveJUnitLaunchArgumentsParams +---@field project_name string +---@field test_names string[] +---@field test_level java-core.TestLevel +---@field test_kind java-core.TestKind + +---Returns junit launch arguments +---@param args java-core.JavaCoreTestResolveJUnitLaunchArgumentsParams +---@return java-core.JavaCoreTestJunitLaunchArguments # junit launch arguments +function TestClient:resolve_junit_launch_arguments(args) + local launch_args = self:workspace_execute_command( + 'vscode.java.test.junit.argument', + ---@diagnostic disable-next-line: param-type-mismatch + vim.fn.json_encode(args) + ) + + if not launch_args or not launch_args.body then + local msg = 'Failed to retrieve JUnit launch arguments' + + log.error(msg, launch_args) + error(msg) + end + + return launch_args.body +end + +---@enum java-core.TestKind +TestClient.TestKind = { + JUnit5 = 0, + JUnit = 1, + TestNG = 2, + None = 100, +} + +---@enum java-core.TestLevel +TestClient.TestLevel = { + Root = 0, + Workspace = 1, + WorkspaceFolder = 2, + Project = 3, + Package = 4, + Class = 5, + Method = 6, + Invocation = 7, +} + +return TestClient diff --git a/lua/java-core/ls/clients/jdtls-client.lua b/lua/java-core/ls/clients/jdtls-client.lua new file mode 100644 index 0000000..6a33eac --- /dev/null +++ b/lua/java-core/ls/clients/jdtls-client.lua @@ -0,0 +1,385 @@ +local log = require('java-core.utils.log2') +local class = require('java-core.utils.class') +local await = require('async.waits.wait_with_error_handler') + +---@alias java-core.JdtlsRequestMethod +---| 'workspace/executeCommand' +---| 'java/inferSelection' +---| 'java/getRefactorEdit' +---| 'java/buildWorkspace' +---| 'java/checkConstructorsStatus' +---| 'java/generateConstructors' +---| 'java/checkToStringStatus' +---| 'java/generateToString' +---| 'java/checkHashCodeEqualsStatus' +---| 'java/generateHashCodeEquals' +---| 'java/checkDelegateMethodsStatus' +---| 'java/generateDelegateMethods' +---| 'java/move' +---| 'java/searchSymbols' +---| 'java/getMoveDestinations' +---| 'java/listOverridableMethods' +---| 'java/addOverridableMethods' + +---@alias jdtls.CodeActionCommand +---| 'extractVariable' +---| 'assignVariable' +---| 'extractVariableAllOccurrence' +---| 'extractConstant' +---| 'extractMethod' +---| 'extractField' +---| 'extractInterface' +---| 'changeSignature' +---| 'assignField' +---| 'convertVariableToField' +---| 'invertVariable' +---| 'introduceParameter' +---| 'convertAnonymousClassToNestedCommand' + +---@class jdtls.RefactorWorkspaceEdit +---@field edit lsp.WorkspaceEdit +---@field command? lsp.Command +---@field errorMessage? string + +---@class jdtls.SelectionInfo +---@field name string +---@field length number +---@field offset number +---@field params? string[] + +---@class java-core.JdtlsClient +---@field client vim.lsp.Client +local JdtlsClient = class() + +function JdtlsClient:_init(client) + self.client = client +end + +---Sends a LSP request +---@param method java-core.JdtlsRequestMethod +---@param params table +---@param buffer? number +function JdtlsClient:request(method, params, buffer) + log.debug('sending LSP request: ' .. method) + + return await(function(callback) + local on_response = function(err, result) + if err then + log.error(method .. ' failed! arguments: ', params, ' error: ', err) + else + log.debug(method .. ' success! response: ', result) + end + + callback(err, result) + end + + ---@diagnostic disable-next-line: param-type-mismatch + return self.client:request(method, params, on_response, buffer) + end) +end + +--- Sends a notification to an LSP server. +--- +--- @param method vim.lsp.protocol.Method.ClientToServer.Notification LSP method name. +--- @param params table? LSP request params. +--- @return boolean status indicating if the notification was successful. +--- If it is false, then the client has shutdown. +function JdtlsClient:notify(method, params) + log.debug('sending LSP notify: ' .. method) + return self.client:notify(method, params) +end + +---Executes a workspace/executeCommand and returns the result +---@param command string workspace command to execute +---@param arguments? lsp.LSPAny[] +---@param buffer? integer +---@return lsp.LSPAny +function JdtlsClient:workspace_execute_command(command, arguments, buffer) + return await(function(callback) + self.client:exec_cmd( + ---@diagnostic disable-next-line: missing-fields + { command = command, arguments = arguments }, + { bufnr = buffer }, + callback + ) + end) +end + +---@class jdtls.ResourceMoveDestination +---@field displayName string +---@field isDefaultPackage boolean +---@field isParentOfSelectedFile boolean +---@field path string +---@field project string +---@field uri string + +---@class jdtls.InstanceMethodMoveDestination +---@field bindingKey string +---@field isField boolean +---@field isSelected boolean +---@field name string +---@field type string + +---@class jdtls.listOverridableMethodsResponse +---@field methods jdtls.OverridableMethod[] +---@field type string + +---@class jdtls.OverridableMethod +---@field key string +---@field bindingKey string +---@field declaringClass string +---@field declaringClassType string +---@field name string +---@field parameters string[] +---@field unimplemented boolean + +---@class jdtls.MoveDestinationsResponse +---@field errorMessage? string +---@field destinations jdtls.InstanceMethodMoveDestination[]|jdtls.ResourceMoveDestination[] + +---@class jdtls.ImportCandidate +---@field fullyQualifiedName string +---@field id string + +---@class jdtls.ImportSelection +---@field candidates jdtls.ImportCandidate[] +---@field range Range + +---@param params jdtls.MoveParams +---@return jdtls.MoveDestinationsResponse +function JdtlsClient:get_move_destination(params) + return self:request('java/getMoveDestinations', params) +end + +---@class jdtls.MoveParams +---@field moveKind string +---@field sourceUris string[] +---@field params lsp.CodeActionParams | nil +---@field destination? any +---@field updateReferences? boolean + +---@param params jdtls.MoveParams +---@return jdtls.RefactorWorkspaceEdit +function JdtlsClient:java_move(params) + return self:request('java/move', params) +end + +---@class jdtls.SearchSymbolParams: lsp.WorkspaceSymbolParams +---@field projectName string +---@field maxResults? number +---@field sourceOnly? boolean + +---@param params jdtls.SearchSymbolParams +---@return lsp.SymbolInformation +function JdtlsClient:java_search_symbols(params) + return self:request('java/searchSymbols', params) +end + +---Returns more information about the object the cursor is on +---@param command jdtls.CodeActionCommand +---@param params lsp.CodeActionParams +---@param buffer? number +---@return jdtls.SelectionInfo[] +function JdtlsClient:java_infer_selection(command, params, buffer) + return self:request('java/inferSelection', { + command = command, + context = params, + }, buffer) +end + +--- @class jdtls.VariableBinding +--- @field bindingKey string +--- @field name string +--- @field type string +--- @field isField boolean +--- @field isSelected? boolean + +---@class jdtls.MethodBinding +---@field bindingKey string; +---@field name string; +---@field parameters string[]; + +---@class jdtls.JavaCheckConstructorsStatusResponse +---@field constructors jdtls.MethodBinding +---@field fields jdtls.MethodBinding + +---@param params lsp.CodeActionParams +---@return jdtls.JavaCheckConstructorsStatusResponse +function JdtlsClient:java_check_constructors_status(params) + return self:request('java/checkConstructorsStatus', params) +end + +---@param params jdtls.GenerateConstructorsParams +---@return lsp.WorkspaceEdit +function JdtlsClient:java_generate_constructor(params) + return self:request('java/generateConstructors', params) +end + +---@class jdtls.CheckToStringResponse +---@field type string +---@field fields jdtls.VariableBinding[] +---@field exists boolean + +---@param params lsp.CodeActionParams +---@return jdtls.CheckToStringResponse +function JdtlsClient:java_check_to_string_status(params) + return self:request('java/checkToStringStatus', params) +end + +---@class jdtls.GenerateToStringParams +---@field context lsp.CodeActionParams +---@field fields jdtls.VariableBinding[] + +---@param params jdtls.GenerateToStringParams +---@return lsp.WorkspaceEdit +function JdtlsClient:java_generate_to_string(params) + return self:request('java/generateToString', params) +end + +---@class jdtls.CheckHashCodeEqualsResponse +---@field type string +---@field fields jdtls.VariableBinding[] +---@field existingMethods string[] + +---@param params lsp.CodeActionParams +---@return jdtls.CheckHashCodeEqualsResponse +function JdtlsClient:java_check_hash_code_equals_status(params) + return self:request('java/checkHashCodeEqualsStatus', params) +end + +---@class jdtls.GenerateHashCodeEqualsParams +---@field context lsp.CodeActionParams +---@field fields jdtls.VariableBinding[] +---@field regenerate boolean + +---@param params jdtls.GenerateHashCodeEqualsParams +---@return lsp.WorkspaceEdit +function JdtlsClient:java_generate_hash_code_equals(params) + return self:request('java/generateHashCodeEquals', params) +end + +---@class jdtls.DelegateField +---@field field jdtls.VariableBinding +---@field delegateMethods jdtls.MethodBinding[] + +---@class jdtls.CheckDelegateMethodsResponse +---@field delegateFields jdtls.DelegateField[] + +---@param params lsp.CodeActionParams +---@return jdtls.CheckDelegateMethodsResponse +function JdtlsClient:java_check_delegate_methods_status(params) + return self:request('java/checkDelegateMethodsStatus', params) +end + +---@class jdtls.DelegateEntry +---@field field jdtls.VariableBinding +---@field delegateMethod jdtls.MethodBinding + +---@class jdtls.GenerateDelegateMethodsParams +---@field context lsp.CodeActionParams +---@field delegateEntries jdtls.DelegateEntry[] + +---@param params jdtls.GenerateDelegateMethodsParams +---@return lsp.WorkspaceEdit +function JdtlsClient:java_generate_delegate_methods(params) + return self:request('java/generateDelegateMethods', params) +end + +---@class jdtls.GenerateConstructorsParams +---@field context lsp.CodeActionParams +---@field constructors jdtls.MethodBinding[] +---@field fields jdtls.VariableBinding[] + +---Returns refactor details +---@param command jdtls.CodeActionCommand +---@param action_params lsp.CodeActionParams +---@param formatting_options lsp.FormattingOptions +---@param selection_info jdtls.SelectionInfo[]; +---@param buffer? number +---@return jdtls.RefactorWorkspaceEdit +function JdtlsClient:java_get_refactor_edit(command, action_params, formatting_options, selection_info, buffer) + local params = { + command = command, + context = action_params, + options = formatting_options, + commandArguments = selection_info, + } + + return self:request('java/getRefactorEdit', params, buffer) +end + +---Returns a list of methods that can be overridden +---@param params lsp.CodeActionParams +---@param buffer? number +---@return jdtls.listOverridableMethodsResponse +function JdtlsClient:list_overridable_methods(params, buffer) + return self:request('java/listOverridableMethods', params, buffer) +end + +---Returns a list of methods that can be overridden +---@param context lsp.CodeActionParams +---@param overridable_methods jdtls.OverridableMethod[] +---@param buffer? number +---@return lsp.WorkspaceEdit +function JdtlsClient:add_overridable_methods(context, overridable_methods, buffer) + return self:request('java/addOverridableMethods', { + context = context, + overridableMethods = overridable_methods, + }, buffer) +end + +---Compile the workspace +---@param is_full_compile boolean if true, a complete full compile of the +---workspace will be executed +---@param buffer number +---@return java-core.CompileWorkspaceStatus +function JdtlsClient:java_build_workspace(is_full_compile, buffer) + ---@diagnostic disable-next-line: param-type-mismatch + return self:request('java/buildWorkspace', is_full_compile, buffer) +end + +---Returns the decompiled class file content +---@param uri string uri of the class file +---@return string # decompiled file content +function JdtlsClient:java_decompile(uri) + ---@type string + return self:workspace_execute_command('java.decompile', { uri }) +end + +function JdtlsClient:get_capability(...) + local capability = self.client.server_capabilities + + for _, value in ipairs({ ... }) do + if type(capability) ~= 'table' then + log.fmt_warn('Looking for capability: %s in value %s', value, capability) + return nil + end + + capability = capability[value] + end + + return capability +end + +---Updates JDTLS settings via workspace/didChangeConfiguration +---@param settings JavaConfigurationSettings +---@return boolean +function JdtlsClient:workspace_did_change_configuration(settings) + local params = { settings = settings } + return self:notify('workspace/didChangeConfiguration', params) +end + +---Returns true if the LS supports the given command +---@param command_name string name of the command +---@return boolean # true if the command is supported +function JdtlsClient:has_command(command_name) + local commands = self:get_capability('executeCommandProvider', 'commands') + + if not commands then + return false + end + + return vim.tbl_contains(commands, command_name) +end + +return JdtlsClient diff --git a/lua/java-core/ls/servers/jdtls/cmd.lua b/lua/java-core/ls/servers/jdtls/cmd.lua new file mode 100644 index 0000000..371c358 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -0,0 +1,133 @@ +local List = require('java-core.utils.list') +local path = require('java-core.utils.path') +local Manager = require('pkgm.manager') +local conf = require('java.config') +local system = require('java-core.utils.system') +local log = require('java-core.utils.log2') +local err = require('java-core.utils.errors') +local java_version_map = require('java-core.constants.java_version') +local lsp_utils = require('java-core.utils.lsp') + +local M = {} + +local jdtls_root = Manager:get_install_dir('jdtls', conf.jdtls.version) + +--- Returns a function that returns the command to start jdtls +---@param opts { use_lombok: boolean } +function M.get_cmd(opts) + ---@param dispatchers? vim.lsp.rpc.Dispatchers + ---@param config vim.lsp.ClientConfig + return function(dispatchers, config) + local cmd = M.get_jvm_args(opts):concat(M.get_jar_args()) + + M.validate_java_version() + + log.debug('Starting jdtls with cmd', cmd) + + local result = vim.lsp.rpc.start(cmd, dispatchers, { + cwd = config.cmd_cwd, + env = config.cmd_env, + detached = config.detached, + }) + + return result + end +end + +---@private +---@param opts { use_lombok: boolean } +---@return java-core.List +function M.get_jvm_args(opts) + local jdtls_config = path.join(jdtls_root, system.get_config_suffix()) + + local jvm_args = List:new({ + 'java', + '-Declipse.application=org.eclipse.jdt.ls.core.id1', + '-Dosgi.bundles.defaultStartLevel=4', + '-Declipse.product=org.eclipse.jdt.ls.core.product', + '-Dosgi.checkConfiguration=true', + '-Dosgi.sharedConfiguration.area=' .. jdtls_config, + '-Dosgi.sharedConfiguration.area.readOnly=true', + '-Dosgi.configuration.cascaded=true', + '-Xms1G', + '--add-modules=ALL-SYSTEM', + + '--add-opens', + 'java.base/java.util=ALL-UNNAMED', + + '--add-opens', + 'java.base/java.lang=ALL-UNNAMED', + }) + + -- Adding lombok + if opts.use_lombok then + local lombok_root = Manager:get_install_dir('lombok', conf.lombok.version) + local lombok_path = vim.fn.glob(path.join(lombok_root, 'lombok*.jar')) + jvm_args:push('-javaagent:' .. lombok_path) + end + + return jvm_args +end + +---@private +---@param cwd? string +---@return java-core.List +function M.get_jar_args(cwd) + cwd = cwd or vim.fn.getcwd() + + local launcher_reg = path.join(jdtls_root, 'plugins', 'org.eclipse.equinox.launcher_*.jar') + local equinox_launcher = vim.fn.glob(path.join(jdtls_root, 'plugins', 'org.eclipse.equinox.launcher_*.jar')) + + if equinox_launcher == '' then + -- stylua: ignore + local msg = string.format('JDTLS equinox launcher not found. Expected path: %s. ', launcher_reg) + err.throw(msg) + end + + return List:new({ + '-jar', + equinox_launcher, + + '-configuration', + lsp_utils.get_jdtls_cache_conf_path(), + + '-data', + lsp_utils.get_jdtls_cache_data_path(cwd), + }) +end + +---@private +function M.validate_java_version() + local v = M.get_java_major_version() + local exp_ver = java_version_map[conf.jdtls.version] + + if v <= exp_ver.from and v >= exp_ver.to then + local msg = string.format( + 'Java version mismatch: JDTLS %s requires Java %d - %d, but found Java %d', + conf.jdtls.version, + exp_ver.from, + exp_ver.to, + v + ) + + err.throw(msg) + end +end + +---@private +function M.get_java_major_version() + local version = vim.fn.system('java -version') + local major = version:match('version (%d+)') + or version:match('version "(%d+)') + or version:match('openjdk (%d+)') + or version:match('java (%d+)') + + if major then + return tonumber(major) + end + + local msg = 'Could not determine java version from::' .. version + err.throw(msg) +end + +return M diff --git a/lua/java-core/ls/servers/jdtls/conf.lua b/lua/java-core/ls/servers/jdtls/conf.lua new file mode 100644 index 0000000..bf5cf2c --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/conf.lua @@ -0,0 +1,33 @@ +return { + init_options = { + extendedClientCapabilities = { + actionableRuntimeNotificationSupport = true, + advancedExtractRefactoringSupport = true, + advancedGenerateAccessorsSupport = true, + advancedIntroduceParameterRefactoringSupport = true, + advancedOrganizeImportsSupport = true, + advancedUpgradeGradleSupport = true, + classFileContentsSupport = true, + clientDocumentSymbolProvider = true, + clientHoverProvider = false, + executeClientCommandSupport = true, + extractInterfaceSupport = true, + generateConstructorsPromptSupport = true, + generateDelegateMethodsPromptSupport = true, + generateToStringPromptSupport = true, + gradleChecksumWrapperPromptSupport = true, + hashCodeEqualsPromptSupport = true, + inferSelectionSupport = { + 'extractConstant', + 'extractField', + 'extractInterface', + 'extractMethod', + 'extractVariableAllOccurrence', + 'extractVariable', + }, + moveRefactoringSupport = true, + onCompletionItemSelectedCommand = 'editor.action.triggerParameterHints', + overrideMethodsPromptSupport = true, + }, + }, +} diff --git a/lua/java-core/ls/servers/jdtls/env.lua b/lua/java-core/ls/servers/jdtls/env.lua new file mode 100644 index 0000000..3e1bed2 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/env.lua @@ -0,0 +1,32 @@ +local path = require('java-core.utils.path') +local Manager = require('pkgm.manager') +local log = require('java-core.utils.log2') + +--- @TODO: importing stuff from java main package feels wrong. +--- We should fix this in the future +local config = require('java.config') + +local M = {} + +--- @param opts { use_jdk: boolean } +function M.get_env(opts) + if not opts.use_jdk then + log.debug('use_jdk disabled, returning empty env') + return {} + end + + local jdk_root = Manager:get_install_dir('openjdk', config.jdk.version) + local java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) + local java_bin = path.join(java_home, 'bin') + + local env = { + ['PATH'] = java_bin .. ':' .. vim.fn.getenv('PATH'), + ['JAVA_HOME'] = java_home, + } + + log.debug('env set - JAVA_HOME:', env.JAVA_HOME, 'PATH:', env.PATH) + + return env +end + +return M diff --git a/lua/java-core/ls/servers/jdtls/init.lua b/lua/java-core/ls/servers/jdtls/init.lua new file mode 100644 index 0000000..8367a36 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/init.lua @@ -0,0 +1,26 @@ +local M = {} + +--- Returns jdtls config +---@param opts { use_jdk: boolean, use_lombok: boolean, plugins: string[] } +function M.get_config(opts) + local conf = require('java-core.ls.servers.jdtls.conf') + local plugins = require('java-core.ls.servers.jdtls.plugins') + local cmd = require('java-core.ls.servers.jdtls.cmd') + local env = require('java-core.ls.servers.jdtls.env') + local root = require('java-core.ls.servers.jdtls.root') + local log = require('java-core.utils.log2') + + log.debug('get_config called with opts:', opts) + + local base_conf = vim.deepcopy(conf, true) + + base_conf.cmd = cmd.get_cmd(opts) + base_conf.cmd_env = env.get_env(opts) + base_conf.init_options.bundles = plugins.get_plugins(opts) + base_conf.filetypes = { 'java' } + base_conf.root_markers = root.get_root_markers() + + return base_conf +end + +return M diff --git a/lua/java-core/ls/servers/jdtls/plugins.lua b/lua/java-core/ls/servers/jdtls/plugins.lua new file mode 100644 index 0000000..086b02c --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/plugins.lua @@ -0,0 +1,66 @@ +local file = require('java-core.utils.file') +local List = require('java-core.utils.list') +local Manager = require('pkgm.manager') +local log = require('java-core.utils.log2') + +--- @TODO: importing stuff from java main package feels wrong. +--- We should fix this in the future +local config = require('java.config') + +local M = {} + +local plug_jar_map = { + ['java-test'] = { + 'extension/server/junit-jupiter-api_*.jar', + 'extension/server/junit-jupiter-engine_*.jar', + 'extension/server/junit-jupiter-migrationsupport_*.jar', + 'extension/server/junit-jupiter-params_*.jar', + 'extension/server/junit-platform-commons_*.jar', + 'extension/server/junit-platform-engine_*.jar', + 'extension/server/junit-platform-launcher_*.jar', + 'extension/server/junit-platform-runner_*.jar', + 'extension/server/junit-platform-suite-api_*.jar', + 'extension/server/junit-platform-suite-commons_*.jar', + 'extension/server/junit-platform-suite-engine_*.jar', + 'extension/server/junit-vintage-engine_*.jar', + 'extension/server/org.apiguardian.api_*.jar', + 'extension/server/org.eclipse.jdt.junit4.runtime_*.jar', + 'extension/server/org.eclipse.jdt.junit5.runtime_*.jar', + 'extension/server/org.opentest4j_*.jar', + 'extension/server/com.microsoft.java.test.plugin-*.jar', + }, + ['java-debug'] = { + 'extension/server/com.microsoft.java.debug.plugin-*.jar', + }, + ['spring-boot-tools'] = { 'extension/jars/*.jar' }, +} + +local plugin_version_map = { + ['java-test'] = config.java_test.version, + ['java-debug'] = config.java_debug_adapter.version, + ['spring-boot-tools'] = config.spring_boot_tools.version, +} + +---Returns a list of .jar file paths for given list of jdtls plugins +---@param opts { plugins: string[] } +---@return string[] # list of .jar file paths +function M.get_plugins(opts) + return List:new(opts.plugins) + :map(function(plugin_name) + local version = plugin_version_map[plugin_name] + local root = Manager:get_install_dir(plugin_name, version) + local jars = file.resolve_paths(root, plug_jar_map[plugin_name]) + + if #jars == 0 then + -- stylua: ignore + log.error(string.format( 'No jars found for plugin "%s" (version: %s) at %s', plugin_name, version, root)) + -- stylua: ignore + error(string.format( 'Failed to load plugin "%s". No jars found at %s', plugin_name, root)) + end + + return jars + end) + :flatten() +end + +return M diff --git a/lua/java-core/ls/servers/jdtls/root.lua b/lua/java-core/ls/servers/jdtls/root.lua new file mode 100644 index 0000000..beda3f2 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/root.lua @@ -0,0 +1,29 @@ +local M = {} + +local root_markers1 = { + -- Multi-module projects + 'mvnw', -- Maven + 'gradlew', -- Gradle + 'settings.gradle', -- Gradle + 'settings.gradle.kts', -- Gradle + -- Use git directory as last resort for multi-module maven projects + -- In multi-module maven projects it is not really possible to determine what is the parent directory + -- and what is submodule directory. And jdtls does not break if the parent directory is at higher level than + -- actual parent pom.xml so propagating all the way to root git directory is fine + '.git', +} + +local root_markers2 = { + -- Single-module projects + 'build.xml', -- Ant + 'pom.xml', -- Maven + 'build.gradle', -- Gradle + 'build.gradle.kts', -- Gradle +} + +function M.get_root_markers() + return vim.fn.has('nvim-0.11.3') == 1 and { root_markers1, root_markers2 } + or vim.list_extend(root_markers1, root_markers2) +end + +return M diff --git a/lua/java-core/types/jdtls-types.lua b/lua/java-core/types/jdtls-types.lua new file mode 100644 index 0000000..d6749a6 --- /dev/null +++ b/lua/java-core/types/jdtls-types.lua @@ -0,0 +1,11 @@ +local M = {} + +---@enum java-core.CompileWorkspaceStatus +M.CompileWorkspaceStatus = { + FAILED = 0, + SUCCEED = 1, + WITHERROR = 2, + CANCELLED = 3, +} + +return M diff --git a/lua/java-core/types/nvim-types.lua b/lua/java-core/types/nvim-types.lua new file mode 100644 index 0000000..d16632b --- /dev/null +++ b/lua/java-core/types/nvim-types.lua @@ -0,0 +1,6 @@ +---@class nvim.CodeActionParamsResponse +---@field bufnr number +---@field client_id number +---@field method string +---@field params lsp.CodeActionParams +---@field version number diff --git a/lua/java/utils/buffer.lua b/lua/java-core/utils/buffer.lua similarity index 100% rename from lua/java/utils/buffer.lua rename to lua/java-core/utils/buffer.lua diff --git a/lua/java-core/utils/class.lua b/lua/java-core/utils/class.lua new file mode 100644 index 0000000..cd9ec76 --- /dev/null +++ b/lua/java-core/utils/class.lua @@ -0,0 +1,280 @@ +--- Provides a reuseable and convenient framework for creating classes in Lua. +-- Two possible notations: +-- +-- B = class(A) +-- class.B(A) +-- +-- The latter form creates a named class within the current environment. Note +-- that this implicitly brings in `pl.utils` as a dependency. +-- +-- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion} +-- @module pl.class + +local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type = + _G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type +local compat + +-- this trickery is necessary to prevent the inheritance of 'super' and +-- the resulting recursive call problems. +local function call_ctor(c, obj, ...) + local init = rawget(c, '_init') + local parent_with_init = rawget(c, '_parent_with_init') + + if parent_with_init then + if not init then -- inheriting an init + init = rawget(parent_with_init, '_init') + parent_with_init = rawget(parent_with_init, '_parent_with_init') + end + if parent_with_init then -- super() points to one above whereever _init came from + rawset(obj, 'super', function(loc_obj, ...) + call_ctor(parent_with_init, loc_obj, ...) + end) + end + else + -- Without this, calling super() where none exists will sometimes loop and stack overflow + rawset(obj, 'super', nil) + end + + local res = init(obj, ...) + if parent_with_init then -- If this execution of call_ctor set a super, unset it + rawset(obj, 'super', nil) + end + + return res +end + +--- initializes an __instance__ upon creation. +-- @function class:_init +-- @param ... parameters passed to the constructor +-- @usage local Cat = class() +-- function Cat:_init(name) +-- --self:super(name) -- call the ancestor initializer if needed +-- self.name = name +-- end +-- +-- local pussycat = Cat("pussycat") +-- print(pussycat.name) --> pussycat + +--- checks whether an __instance__ is derived from some class. +-- Works the other way around as `class_of`. It has two ways of using; +-- 1) call with a class to check against, 2) call without params. +-- @function instance:is_a +-- @param some_class class to check against, or `nil` to return the class +-- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then +-- it returns the class table of the instance +-- @usage local pussycat = Lion() -- assuming Lion derives from Cat +-- if pussycat:is_a(Cat) then +-- -- it's true, it is a Lion, but also a Cat +-- end +-- +-- if pussycat:is_a() == Lion then +-- -- It's true +-- end +local function is_a(self, klass) + if klass == nil then + -- no class provided, so return the class this instance is derived from + return getmetatable(self) + end + local m = getmetatable(self) + if not m then + return false + end --*can't be an object! + while m do + if m == klass then + return true + end + m = rawget(m, '_base') + end + return false +end + +--- checks whether an __instance__ is derived from some class. +-- Works the other way around as `is_a`. +-- @function some_class:class_of +-- @param some_instance instance to check against +-- @return `true` if `some_instance` is derived from `some_class` +-- @usage local pussycat = Lion() -- assuming Lion derives from Cat +-- if Cat:class_of(pussycat) then +-- -- it's true +-- end +local function class_of(klass, obj) + if type(klass) ~= 'table' or not rawget(klass, 'is_a') then + return false + end + return klass.is_a(obj, klass) +end + +--- cast an object to another class. +-- It is not clever (or safe!) so use carefully. +-- @param some_instance the object to be changed +-- @function some_class:cast +local function cast(klass, obj) + return setmetatable(obj, klass) +end + +local function _class_tostring(obj) + local mt = obj._class + local name = rawget(mt, '_name') + setmetatable(obj, nil) + local str = tostring(obj) + setmetatable(obj, mt) + if name then + str = name .. str:gsub('table', '') + end + return str +end + +local function tupdate(td, ts, dont_override) + for k, v in pairs(ts) do + if not dont_override or td[k] == nil then + td[k] = v + end + end +end + +local function _class(base, c_arg, c) + -- the class `c` will be the metatable for all its objects, + -- and they will look up their methods in it. + local mt = {} -- a metatable for the class to support __call and _handler + -- can define class by passing it a plain table of methods + local plain = type(base) == 'table' and not getmetatable(base) + if plain then + c = base + base = c._base + else + c = c or {} + end + + if type(base) == 'table' then + -- our new class is a shallow copy of the base class! + -- but be careful not to wipe out any methods we have been given at this point! + tupdate(c, base, plain) + c._base = base + -- inherit the 'not found' handler, if present + if rawget(c, '_handler') then + mt.__index = c._handler + end + elseif base ~= nil then + error('must derive from a table type', 3) + end + + c.__index = c + setmetatable(c, mt) + if not plain then + if base and rawget(base, '_init') then + c._parent_with_init = base + end -- For super and inherited init + c._init = nil + end + + if base and rawget(base, '_class_init') then + base._class_init(c, c_arg) + end + + -- expose a ctor which can be called by () + mt.__call = function(_class_tbl, ...) + local obj + if rawget(c, '_create') then + obj = c._create(...) + end + if not obj then + obj = {} + end + setmetatable(obj, c) + + if rawget(c, '_init') or rawget(c, '_parent_with_init') then -- constructor exists + local res = call_ctor(c, obj, ...) + if res then -- _if_ a ctor returns a value, it becomes the object... + obj = res + setmetatable(obj, c) + end + end + + if base and rawget(base, '_post_init') then + base._post_init(obj) + end + + return obj + end + -- Call Class.catch to set a handler for methods/properties not found in the class! + c.catch = function(self, handler) + if type(self) == 'function' then + -- called using . instead of : + handler = self + end + c._handler = handler + mt.__index = handler + end + c.is_a = is_a + c.class_of = class_of + c.cast = cast + c._class = c + + if not rawget(c, '__tostring') then + c.__tostring = _class_tostring + end + + return c +end + +--- create a new class, derived from a given base class. +-- Supporting two class creation syntaxes: +-- either `Name = class(base)` or `class.Name(base)`. +-- The first form returns the class directly and does not set its `_name`. +-- The second form creates a variable `Name` in the current environment set +-- to the class, and also sets `_name`. +-- @function class +-- @param base optional base class +-- @param c_arg optional parameter to class constructor +-- @param c optional table to be used as class +local class +class = setmetatable({}, { + __call = function(_fun, ...) + return _class(...) + end, + __index = function(_tbl, key) + if key == 'class' then + io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n') + return class + end + compat = compat or require('pl.compat') + local env = compat.getfenv(2) + return function(...) + local c = _class(...) + c._name = key + rawset(env, key, c) + return c + end + end, +}) + +class.properties = class() + +function class.properties._class_init(klass) + klass.__index = function(t, key) + -- normal class lookup! + local v = klass[key] + if v then + return v + end + -- is it a getter? + v = rawget(klass, 'get_' .. key) + if v then + return v(t) + end + -- is it a field? + return rawget(t, '_' .. key) + end + klass.__newindex = function(t, key, value) + -- if there's a setter, use that, otherwise directly set table + local p = 'set_' .. key + local setter = klass[p] + if setter then + setter(t, value) + else + rawset(t, key, value) + end + end +end + +return class diff --git a/lua/java-core/utils/command.lua b/lua/java-core/utils/command.lua new file mode 100644 index 0000000..a35c7d8 --- /dev/null +++ b/lua/java-core/utils/command.lua @@ -0,0 +1,53 @@ +local M = {} + +---Converts a path array to command name +---@param path string[] +---@return string +function M.path_to_command_name(path) + local name = 'Java' + + for _, word in ipairs(path) do + local sub_words = vim.split(word, '_') + local changed_word = '' + + for _, sub_word in ipairs(sub_words) do + local first_char = sub_word:sub(1, 1):upper() + local rest = sub_word:sub(2) + changed_word = changed_word .. first_char .. rest + end + + name = name .. changed_word + end + + return name +end + +---Registers an API by creating a user command and adding to module table +---@param module table +---@param path string[] +---@param command fun() +---@param opts vim.api.keyset.user_command +function M.register_api(module, path, command, opts) + local name = M.path_to_command_name(path) + + vim.api.nvim_create_user_command(name, command, opts or {}) + + local last_index = #path + local func_name = path[last_index] + + table.remove(path, last_index) + + local node = module + + for _, v in ipairs(path) do + if not node[v] then + node[v] = {} + end + + node = node[v] + end + + node[func_name] = command +end + +return M diff --git a/lua/java/handlers/error.lua b/lua/java-core/utils/error_handler.lua similarity index 78% rename from lua/java/handlers/error.lua rename to lua/java-core/utils/error_handler.lua index 45aa6de..bc0f096 100644 --- a/lua/java/handlers/error.lua +++ b/lua/java-core/utils/error_handler.lua @@ -1,5 +1,4 @@ local notify = require('java-core.utils.notify') -local log = require('java.utils.log') local function table_tostring(tbl) local str = '' @@ -12,8 +11,9 @@ end ---Returns a error handler ---@param msg string messages to show in the error +---@param log table|nil log instance to use (optional, defaults to no logging) ---@return fun(err: any) # function that log and notify the error -local function get_error_handler(msg) +local function get_error_handler(msg, log) return function(err) local trace = debug.traceback() @@ -23,7 +23,9 @@ local function get_error_handler(msg) local log_str = table_tostring(log_obj) - log.error(log_str) + if log then + log.error(log_str) + end notify.error(log_str) error(log_str) end diff --git a/lua/java-core/utils/errors.lua b/lua/java-core/utils/errors.lua new file mode 100644 index 0000000..65fddb4 --- /dev/null +++ b/lua/java-core/utils/errors.lua @@ -0,0 +1,14 @@ +local notify = require('java-core.utils.notify') +local log = require('java-core.utils.log2') + +local M = {} + +---Notifies user, logs error, and throws error +---@param msg string error message +function M.throw(msg) + notify.error(msg) + log.error(msg) + error(msg) +end + +return M diff --git a/lua/java-core/utils/event.lua b/lua/java-core/utils/event.lua new file mode 100644 index 0000000..e175839 --- /dev/null +++ b/lua/java-core/utils/event.lua @@ -0,0 +1,23 @@ +local M = {} + +---@param opts { once?: boolean, group?: number, callback: fun(client: vim.lsp.Client) } +function M.on_jdtls_attach(opts) + local id + + id = vim.api.nvim_create_autocmd('LspAttach', { + group = opts.group, + callback = function(args) + local client = vim.lsp.get_client_by_id(args.data.client_id) + + if client and client.name == 'jdtls' then + opts.callback(client) + + if opts.once then + vim.api.nvim_del_autocmd(id) + end + end + end, + }) +end + +return M diff --git a/lua/java-core/utils/file.lua b/lua/java-core/utils/file.lua new file mode 100644 index 0000000..c439548 --- /dev/null +++ b/lua/java-core/utils/file.lua @@ -0,0 +1,15 @@ +local path = require('java-core.utils.path') + +local List = require('java-core.utils.list') + +local M = {} + +function M.resolve_paths(root, paths) + return List:new(paths) + :map(function(path_pattern) + return vim.fn.glob(path.join(root, path_pattern), true, true) + end) + :flatten() +end + +return M diff --git a/lua/java-core/utils/list.lua b/lua/java-core/utils/list.lua new file mode 100644 index 0000000..250e2c2 --- /dev/null +++ b/lua/java-core/utils/list.lua @@ -0,0 +1,185 @@ +---@class java-core.List +local M = {} + +---Returns a new list +---@param o? table +---@return java-core.List +function M:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +---Appends a value into to the list +---@param value any +function M:push(value) + table.insert(self, value) +end + +---Appends a value into to the list +---@param value any +function M:push_if(condition, value) + if condition then + self:push(value) + end +end + +---Finds the matching value in a list +---@param finder fun(value: any): boolean +---@return any +function M:find(finder) + for _, value in ipairs(self) do + if finder(value) then + return value + end + end + + return nil +end + +--- Finds the matching value in a list before a given index +--- @param finder fun(value: any): boolean +--- @return boolean +function M:contains(finder) + if type(finder) == 'function' then + for _, value in ipairs(self) do + if finder(value) then + return true + end + end + else + for _, value in ipairs(self) do + if value == finder then + return true + end + end + end + + return false +end + +--- Finds the matching value in a list after a given index +--- +--- @param finder fun(value: any): boolean +--- @return number +function M:find_index(finder) + for i, value in ipairs(self) do + if finder(value) then + return i + end + end + + return -1 +end + +--- Finds the matching value in a list after a given index +---@param index number +---@param finder fun(value: any): boolean +---@return any|nil +function M:find_after(index, finder) + for i, value in ipairs(self) do + if i > index and finder(value) then + return value + end + end +end + +---Returns a list of mapped values +---@param mapper fun(value: any, index: number): any +---@return java-core.List +function M:map(mapper) + local mapped = M:new() + + for i, v in ipairs(self) do + mapped:push(mapper(v, i)) + end + + return mapped +end + +---Flatten a list +---@return java-core.List +function M:flatten() + local flatten = M:new() + + for _, v1 in ipairs(self) do + for _, v2 in ipairs(v1) do + flatten:push(v2) + end + end + + return flatten +end + +---Merge a given list values to current list +---@param list any[] +---@return java-core.List +function M:concat(list) + local new_list = M:new() + + for _, v in ipairs(self) do + new_list:push(v) + end + + for _, v in ipairs(list) do + new_list:push(v) + end + + return new_list +end + +function M:includes(value) + for _, v in ipairs(self) do + if v == value then + return true + end + end + + return false +end + +---Join list items to a string +---@param separator string +---@return string +function M:join(separator) + return table.concat(self, separator) +end + +---Calls the given callback for each element +---@param callback fun(value: any, index: integer) +function M:for_each(callback) + for i, v in ipairs(self) do + callback(v, i) + end +end + +---Returns true if every element in the list passes the validation +---@param validator fun(value: any, index: integer): boolean +---@return boolean +function M:every(validator) + for i, v in ipairs(self) do + if not validator(v, i) then + return false + end + end + + return true +end + +---Returns a filtered list +---@param filter fun(value: any, index: integer): boolean +---@return java-core.List +function M:filter(filter) + local new_list = M:new() + + self:for_each(function(value, index) + if filter(value, index) then + new_list:push(value) + end + end) + + return new_list +end + +return M diff --git a/lua/java/utils/log.lua b/lua/java-core/utils/log.lua similarity index 86% rename from lua/java/utils/log.lua rename to lua/java-core/utils/log.lua index 9eb90e5..1d10846 100644 --- a/lua/java/utils/log.lua +++ b/lua/java-core/utils/log.lua @@ -16,16 +16,16 @@ local default_config = { plugin = 'nvim-java', -- Should print the output to neovim while running - use_console = false, + use_console = true, -- Should highlighting be used in console (using echohl) highlights = true, -- Should write to a file - use_file = true, + use_file = false, -- Any messages above this level will be logged. - level = 'trace', + level = 'debug', -- Level configuration modes = { @@ -49,11 +49,7 @@ local unpack = unpack or table.unpack log.new = function(config, standalone) config = vim.tbl_deep_extend('force', default_config, config) - local outfile = string.format( - '%s/%s.log', - vim.api.nvim_call_function('stdpath', { 'data' }), - config.plugin - ) + local outfile = string.format('%s/%s.log', vim.api.nvim_call_function('stdpath', { 'data' }), config.plugin) local obj if standalone then @@ -104,13 +100,7 @@ log.new = function(config, standalone) -- Output to console if config.use_console then - local console_string = string.format( - '[%-6s%s] %s: %s', - nameupper, - os.date('%H:%M:%S'), - lineinfo, - msg - ) + local console_string = string.format('[%-6s%s] %s: %s', nameupper, os.date('%H:%M:%S'), lineinfo, msg) if config.highlights and level_config.hl then vim.cmd(string.format('echohl %s', level_config.hl)) @@ -118,13 +108,7 @@ log.new = function(config, standalone) local split_console = vim.split(console_string, '\n') for _, v in ipairs(split_console) do - vim.cmd( - string.format( - [[echom "[%s] %s"]], - config.plugin, - vim.fn.escape(v, '"') - ) - ) + vim.cmd(string.format([[echom "[%s] %s"]], config.plugin, vim.fn.escape(v, '"'))) end if config.highlights and level_config.hl then @@ -135,9 +119,10 @@ log.new = function(config, standalone) -- Output to log file if config.use_file then local fp = io.open(outfile, 'a') - local str = - string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) + local str = string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) + assert(fp, 'cannot open file: ' .. ' to write logs') + fp:write(str) fp:close() end diff --git a/lua/java-core/utils/log2.lua b/lua/java-core/utils/log2.lua new file mode 100644 index 0000000..8fffeeb --- /dev/null +++ b/lua/java-core/utils/log2.lua @@ -0,0 +1,147 @@ +local M = {} + +---@alias java-core.LogLevel 'trace'|'debug'|'info'|'warn'|'error'|'fatal' + +---@class java-core.Log2Config +---@field use_console boolean Enable console logging +---@field use_file boolean Enable file logging +---@field level java-core.LogLevel Minimum log level to display +---@field log_file string Path to log file +---@field max_lines number Maximum lines to keep in log file +---@field show_location boolean Show file location in log messages + +---@class java-core.PartialLog2Config +---@field use_console? boolean Enable console logging +---@field use_file? boolean Enable file logging +---@field level? java-core.LogLevel Minimum log level to display +---@field log_file? string Path to log file +---@field max_lines? number Maximum lines to keep in log file +---@field show_location? boolean Show file location in log messages + +---@type java-core.Log2Config +local default_config = { + use_console = false, + use_file = true, + level = 'info', + log_file = vim.fn.stdpath('log') .. '/nvim-java.log', + max_lines = 100, + show_location = false, +} + +---@type java-core.Log2Config +local config = vim.deepcopy(default_config) + +local log_levels = { + trace = 1, + debug = 2, + info = 3, + warn = 4, + error = 5, + fatal = 6, +} + +local highlights = { + trace = 'Comment', + debug = 'Debug', + info = 'DiagnosticInfo', + warn = 'DiagnosticWarn', + error = 'DiagnosticError', + fatal = 'ErrorMsg', +} + +---@param user_config? java-core.PartialLog2Config +function M.setup(user_config) + config = vim.tbl_deep_extend('force', config, user_config or {}) +end + +--- Write message to log file with line limit +---@param msg string +---@private +local function write_to_file(msg) + local log_file = config.log_file + local lines = {} + + local file = io.open(log_file, 'r') + if file then + for line in file:lines() do + table.insert(lines, line) + end + file:close() + end + + table.insert(lines, msg) + + while #lines > config.max_lines do + table.remove(lines, 1) + end + + file = io.open(log_file, 'w') + if file then + for _, line in ipairs(lines) do + file:write(line .. '\n') + end + file:close() + end +end + +--- Log a message +---@param level java-core.LogLevel +---@param ... any +local function log(level, ...) + if log_levels[level] < log_levels[config.level] then + return + end + + local logs = {} + + for _, v in ipairs({ ... }) do + table.insert(logs, vim.inspect(v)) + end + + local location = '' + if config.show_location then + local info = debug.getinfo(3, 'Sl') + if info then + local file = info.short_src or info.source or 'unknown' + local line = info.currentline or 0 + location = '[' .. file .. ':' .. line .. ']' + end + end + + local msg = level:upper() .. (location ~= '' and '::' .. location or '') .. '::' .. table.concat(logs, '::') + + if config.use_console then + local hl = highlights[level] or 'Normal' + vim.api.nvim_echo({ { msg, hl } }, true, {}) + end + + if config.use_file then + write_to_file(msg) + end +end + +function M.info(...) + log('info', ...) +end + +function M.debug(...) + log('debug', ...) +end + +function M.fatal(...) + log('fatal', ...) +end + +function M.error(...) + log('error', ...) +end + +function M.trace(...) + log('trace', ...) +end + +function M.warn(...) + log('warn', ...) +end + +return M diff --git a/lua/java-core/utils/lsp.lua b/lua/java-core/utils/lsp.lua new file mode 100644 index 0000000..af15372 --- /dev/null +++ b/lua/java-core/utils/lsp.lua @@ -0,0 +1,66 @@ +local M = {} + +---Get JDTLS LSP client +---@return vim.lsp.Client +function M.get_jdtls() + local err = require('java-core.utils.errors') + + local clients = vim.lsp.get_clients({ name = 'jdtls' }) + + if #clients == 0 then + vim.print(debug.traceback()) + err.throw('No JDTLS client found') + end + + return clients[1] +end + +--- Returns the path to the jdtls cache directory +---@return string +function M.get_jdtls_cache_root_path() + local path = require('java-core.utils.path') + local cache_root = path.join(vim.fn.stdpath('cache'), 'jdtls') + return cache_root +end + +--- Returns the path to the jdtls config file +---@return string +function M.get_jdtls_cache_conf_path() + local path = require('java-core.utils.path') + local cache_root = M.get_jdtls_cache_root_path() + local conf_path = path.join(cache_root, 'config') + return conf_path +end + +--- Returns the path to the workspace cache directory +---@param cwd string +---@return string +function M.get_jdtls_cache_data_path(cwd) + cwd = cwd or vim.fn.getcwd() + + local path = require('java-core.utils.path') + local cache_root = M.get_jdtls_cache_root_path() + local workspace_path = path.join(cache_root, 'workspace', 'proj_' .. vim.fn.sha256(cwd)) + return workspace_path +end + +---Restart given LSP server +---@param ls string +function M.restart_ls(ls) + if vim.lsp.config[ls] == nil then + vim.notify(("Invalid server name '%s'"):format(ls)) + else + vim.lsp.enable(ls, false) + + vim.iter(vim.lsp.get_clients({ name = ls })):each(function(client) + client:stop(true) + end) + end + + local timer = assert(vim.uv.new_timer()) + timer:start(500, 0, function() + vim.schedule_wrap(vim.lsp.enable)(ls) + end) +end + +return M diff --git a/lua/java-core/utils/notify.lua b/lua/java-core/utils/notify.lua new file mode 100644 index 0000000..ac2d8e1 --- /dev/null +++ b/lua/java-core/utils/notify.lua @@ -0,0 +1,22 @@ +local M = { + opts = {}, +} + +local function index(this, level) + return function(msg, opts) + vim.notify(msg, vim.log.levels[level:upper()], vim.tbl_deep_extend('force', this.opts or {}, opts or {})) + end +end + +setmetatable(M, { + __index = index, + __call = function(_, opts) + return setmetatable({ opts = opts or {} }, { + __index = index, + }) + end, +}) + +return M({ + title = 'Java', +}) diff --git a/lua/java-core/utils/path.lua b/lua/java-core/utils/path.lua new file mode 100644 index 0000000..34129dc --- /dev/null +++ b/lua/java-core/utils/path.lua @@ -0,0 +1,16 @@ +local M = {} + +if vim.fn.has('win32') == 1 or vim.fn.has('win32unix') == 1 then + M.path_separator = '\\' +else + M.path_separator = '/' +end + +---Join a given list of paths to one path +---@param ... string paths to join +---@return string # joined path +function M.join(...) + return table.concat({ ... }, M.path_separator) +end + +return M diff --git a/lua/java-core/utils/set.lua b/lua/java-core/utils/set.lua new file mode 100644 index 0000000..2d0043a --- /dev/null +++ b/lua/java-core/utils/set.lua @@ -0,0 +1,22 @@ +---@class Set: List +local M = {} + +---Returns a new set +---@param o? any[] +---@return Set +function M:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +---Appends a value into to the list if the value is not already in the list +---@param value any +function M:push(value) + if not self:includes(value) then + table.insert(self, value) + end +end + +return M diff --git a/lua/java-core/utils/system.lua b/lua/java-core/utils/system.lua new file mode 100644 index 0000000..b25b4f5 --- /dev/null +++ b/lua/java-core/utils/system.lua @@ -0,0 +1,45 @@ +local M = {} + +function M.get_os() + if vim.fn.has('mac') == 1 then + return 'mac' + elseif vim.fn.has('win32') == 1 or vim.fn.has('win64') == 1 then + return 'win' + else + return 'linux' + end +end + +---@return boolean +function M.is_arm() + local arch = jit.arch + return arch == 'arm' or arch == 'arm64' or arch == 'aarch64' +end + +---@return 'arm'|'x86' +function M.get_arch() + return M.is_arm() and 'arm' or 'x86' +end + +---@return '32bit'|'64bit' +function M.get_bit_depth() + local arch = jit.arch + if arch == 'x64' or arch == 'arm64' or arch == 'aarch64' then + return '64bit' + end + return '32bit' +end + +---@return string +function M.get_config_suffix() + local os = M.get_os() + local suffix = 'config_' .. os + + if os ~= 'win' and M.is_arm() then + suffix = suffix .. '_arm' + end + + return suffix +end + +return M diff --git a/lua/java-dap/data-adapters.lua b/lua/java-dap/data-adapters.lua new file mode 100644 index 0000000..31c4066 --- /dev/null +++ b/lua/java-dap/data-adapters.lua @@ -0,0 +1,44 @@ +local List = require('java-core.utils.list') +local Set = require('java-core.utils.set') + +local M = {} + +---Returns the dap config record +---@param main java-dap.JavaDebugResolveMainClassRecord +---@return java-dap.DapLauncherConfig +function M.main_to_dap_launch_config(main) + local project_name = main.projectName + local main_class = main.mainClass + + return { + request = 'launch', + type = 'java', + name = string.format('%s -> %s', project_name, main_class), + projectName = project_name, + mainClass = main_class, + } +end + +---Returns the launcher config +---@param launch_args java-core.JavaCoreTestJunitLaunchArguments +---@param java_exec string +---@param config { debug: boolean, label: string } +---@return java-dap.DapLauncherConfig +function M.junit_launch_args_to_dap_config(launch_args, java_exec, config) + return { + name = config.label, + type = 'java', + request = 'launch', + mainClass = launch_args.mainClass, + projectName = launch_args.projectName, + noDebug = not config.debug, + javaExec = java_exec, + cwd = launch_args.workingDirectory, + classPaths = Set:new(launch_args.classpath), + modulePaths = Set:new(launch_args.modulepath), + vmArgs = List:new(launch_args.vmArguments):join(' '), + args = List:new(launch_args.programArguments):join(' '), + } +end + +return M diff --git a/lua/java-dap/init.lua b/lua/java-dap/init.lua new file mode 100644 index 0000000..f93db4b --- /dev/null +++ b/lua/java-dap/init.lua @@ -0,0 +1,74 @@ +local M = {} + +function M.setup() + local event = require('java-core.utils.event') + local project_config = require('java.api.profile_config') + local err = require('java-core.utils.errors') + + local has_dap = pcall(require, 'dap') + + if not has_dap then + err.throw([[ + Please install https://github.com/mfussenegger/nvim-dap to enable debugging + or disable the java_debug_adapter.enable option in your config + ]]) + end + + event.on_jdtls_attach({ + callback = function() + project_config.setup() + M.config_dap() + end, + }) +end + +---Configure dap +function M.config_dap() + local get_error_handler = require('java-core.utils.error_handler') + local runner = require('async.runner') + + return runner(function() + local lsp_utils = require('java-core.utils.lsp') + local nvim_dap = require('dap') + local profile_config = require('java.api.profile_config') + local DapSetup = require('java-dap.setup') + + local client = lsp_utils.get_jdtls() + local dap = DapSetup(client) + + ---------------------------------------------------------------------- + -- adapter -- + ---------------------------------------------------------------------- + nvim_dap.adapters.java = function(callback) + runner(function() + local adapter = dap:get_dap_adapter() + callback(adapter --[[@as dap.Adapter]]) + end).run() + end + + ---------------------------------------------------------------------- + -- config -- + ---------------------------------------------------------------------- + + local dap_config = dap:get_dap_config() + + for _, config in ipairs(dap_config) do + local profile = profile_config.get_active_profile(config.name) + if profile then + config.vmArgs = profile.vm_args + config.args = profile.prog_args + end + end + + if nvim_dap.session then + nvim_dap.terminate() + end + + nvim_dap.configurations.java = nvim_dap.configurations.java or {} + vim.list_extend(nvim_dap.configurations.java, dap_config) + end) + .catch(get_error_handler('dap configuration failed')) + .run() +end + +return M diff --git a/lua/java-dap/runner.lua b/lua/java-dap/runner.lua new file mode 100644 index 0000000..1676337 --- /dev/null +++ b/lua/java-dap/runner.lua @@ -0,0 +1,72 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-dap.DapRunner +---@field private server uv_tcp_t +local Runner = class() + +---@return java-dap.DapRunner +function Runner:new() + local o = { + server = nil, + } + + setmetatable(o, self) + self.__index = self + return o +end + +---Dap run with given config +---@param config java-dap.DapLauncherConfig +---@param report java-test.JUnitTestReport +function Runner:run_by_config(config, report) + log.debug('running dap with config: ', config) + + require('dap').run(config --[[@as Configuration]], { + before = function(conf) + return self:before(conf, report) + end, + + after = function() + return self:after() + end, + }) +end + +---Runs before the dap run +---@private +---@param conf java-dap.DapLauncherConfig +---@param report java-test.JUnitTestReport +---@return java-dap.DapLauncherConfig +function Runner:before(conf, report) + log.debug('running "before" callback') + + self.server = assert(vim.loop.new_tcp(), 'uv.new_tcp() must return handle') + self.server:bind('127.0.0.1', 0) + self.server:listen(128, function(err) + assert(not err, err) + + local sock = assert(vim.loop.new_tcp(), 'uv.new_tcp must return handle') + self.server:accept(sock) + local success = sock:read_start(report:get_stream_reader(sock)) + assert(success == 0, 'failed to listen to reader') + end) + + -- replace the port number in the generated args + conf.args = conf.args:gsub('-port ([0-9]+)', '-port ' .. self.server:getsockname().port) + + return conf +end + +---Runs after the dap run +---@private +function Runner:after() + log.debug('running "after" callback') + + if self.server then + self.server:shutdown() + self.server:close() + end +end + +return Runner diff --git a/lua/java-dap/setup.lua b/lua/java-dap/setup.lua new file mode 100644 index 0000000..5973998 --- /dev/null +++ b/lua/java-dap/setup.lua @@ -0,0 +1,125 @@ +local adapters = require('java-dap.data-adapters') +local class = require('java-core.utils.class') +local JavaDebug = require('java-core.ls.clients.java-debug-client') + +---@class java-dap.Setup +---@field private client vim.lsp.Client +---@field private java_debug java-core.DebugClient +local Setup = class() + +---@param client any +function Setup:_init(client) + self.client = client + self.java_debug = JavaDebug(client) +end + +---@class java-dap.DapAdapter +---@field type string +---@field host string +---@field port integer + +---Returns the dap adapter config +---@return java-dap.DapAdapter # dap adapter details +function Setup:get_dap_adapter() + local port = self.java_debug:start_debug_session() + + return { + type = 'server', + host = '127.0.0.1', + port = port, + enrich_config = function(config, callback) + local updated_config = self:enrich_config(config) + callback(updated_config) + end, + } +end + +---@private +---Returns the launch config filled with required data if missing in the passed config +---@param config java-dap.DapLauncherConfigOverridable +---@return java-dap.DapLauncherConfigOverridable +function Setup:enrich_config(config) + config = vim.deepcopy(config) + + local main = config.mainClass + local project = config.projectName + + assert(main, 'To enrich the config, mainClass should already be present') + assert(project, 'To enrich the config, projectName should already be present') + + if config.request == 'launch' then + self.java_debug:build_workspace(main, project, nil, false) + end + + if not config.classPaths or config.modulePaths then + local paths = self.java_debug:resolve_classpath(main, project) + + if not config.modulePaths then + config.modulePaths = paths[1] + end + + if not config.classPaths then + config.classPaths = paths[2] + end + end + + if not config.javaExec then + local java_exec = self.java_debug:resolve_java_executable(main, project) + config.javaExec = java_exec + end + + return config +end + +---Returns the dap configuration for the current project +---@return java-dap.DapLauncherConfig[] # dap configuration details +function Setup:get_dap_config() + local mains = self.java_debug:resolve_main_class() + local config = {} + + for _, main in ipairs(mains) do + table.insert(config, adapters.main_to_dap_launch_config(main)) + end + + return config +end + +return Setup + +---@class java-dap.DapLauncherConfigOverridable +---@field name? string +---@field type? string +---@field request? string +---@field mainClass? string +---@field projectName? string +---@field cwd? string +---@field classPaths? string[] +---@field modulePaths? string[] +---@field vmArgs? string +---@field noDebug? boolean +---@field javaExec? string +---@field args? string +---@field env? { [string]: string; } +---@field envFile? string +---@field sourcePaths? string[] +---@field preLaunchTask? string +---@field postDebugTask? string + +---@class java-dap.DapLauncherConfig +---@field name string +---@field type string +---@field request string +---@field mainClass string +---@field projectName string +---@field cwd string +---@field classPaths string[] +---@field modulePaths string[] +---@field vmArgs string +---@field noDebug boolean +---@field javaExec string +---@field args string +---@field env? { [string]: string; } +---@field envFile? string +---@field sourcePaths string[] +---@field preLaunchTask? string +---@field postDebugTask? string diff --git a/lua/java-refactor/action.lua b/lua/java-refactor/action.lua new file mode 100644 index 0000000..25ba482 --- /dev/null +++ b/lua/java-refactor/action.lua @@ -0,0 +1,303 @@ +local ui = require('java.ui.utils') +local class = require('java-core.utils.class') +local JdtlsClient = require('java-core.ls.clients.jdtls-client') +local RefactorCommands = require('java-refactor.refactor') +local notify = require('java-core.utils.notify') +local List = require('java-core.utils.list') +local lsp_utils = require('java-core.utils.lsp') + +---@class java-refactor.Action +---@field client vim.lsp.Client +---@field jdtls java-core.JdtlsClient +local Action = class() + +---@param client vim.lsp.Client +function Action:_init(client) + self.client = client + self.jdtls = JdtlsClient(client) + self.refactor = RefactorCommands(client) +end + +---@class java-refactor.RenameAction +---@field length number +---@field offset number +---@field uri string + +---@param params java-refactor.RenameAction[] +function Action:rename(params) + for _, rename in ipairs(params) do + local buffer = vim.uri_to_bufnr(rename.uri) + + local line + + vim.api.nvim_buf_call(buffer, function() + line = vim.fn.byte2line(rename.offset) + end) + + local start_char = rename.offset - vim.fn.line2byte(line) + 1 + + vim.api.nvim_win_set_cursor(0, { line, start_char }) + + vim.lsp.buf.rename(nil, { + name = 'jdtls', + bufnr = buffer, + }) + end +end + +---@param params nvim.CodeActionParamsResponse +function Action:generate_constructor(params) + local status = self.jdtls:java_check_constructors_status(params.params) + + if not status or not status.constructors then + return + end + + local selected_constructor = ui.select( + 'Select super class constructor(s).', + status.constructors, + function(constructor) + return string.format('%s %s', constructor.name, table.concat(constructor.parameters, ', ')) + end + ) + + if not selected_constructor then + return + end + + local selected_fields = ui.multi_select('Select Fields:', status.fields, function(field) + return field.name + end) + + local edit = self.jdtls:java_generate_constructor({ + context = params.params, + constructors = { selected_constructor }, + fields = selected_fields or {}, + }) + + vim.lsp.util.apply_workspace_edit(edit, 'utf-8') +end + +---@param params nvim.CodeActionParamsResponse +function Action:generate_to_string(params) + local status = self.jdtls:java_check_to_string_status(params.params) + + if status.exists then + local prompt = string.format( + 'Method "toString()" already exists in the Class %s. Do you want to replace the implementation?', + status.type + ) + local choice = ui.select(prompt, { 'Replace', 'Cancel' }) + + if choice ~= 'Replace' then + return + end + end + + local fields = ui.multi_select( + 'Select the fields to include in the toString() method.', + status.fields, + function(field) + return field.name + end + ) + + if not fields then + return + end + + local edit = self.jdtls:java_generate_to_string({ + context = params.params, + fields = fields, + }) + + vim.lsp.util.apply_workspace_edit(edit, 'utf-8') +end + +---@param params nvim.CodeActionParamsResponse +function Action:generate_hash_code_and_equals(params) + local status = self.jdtls:java_check_hash_code_equals_status(params.params) + + if not status or not status.fields or #status.fields < 1 then + local message = string.format('The operation is not applicable to the type %s.', status.type) + notify.warn(message) + return + end + + local regenerate = false + + if status.existingMethods and #status.existingMethods > 0 then + local prompt = string.format( + 'Methods %s already exists in the Class %s. Do you want to regenerate the implementation?', + 'Regenerate', + 'Cancel' + ) + + local choice = ui.select(prompt, { 'Regenerate', 'Cancel' }) + + if choice == 'Regenerate' then + regenerate = true + end + end + + local fields = ui.multi_select( + 'Select the fields to include in the hashCode() and equals() methods.', + status.fields, + function(field) + return field.name + end + ) + + if not fields or #fields < 1 then + return + end + + local edit = self.jdtls:java_generate_hash_code_equals({ + context = params.params, + fields = fields, + regenerate = regenerate, + }) + + vim.lsp.util.apply_workspace_edit(edit, 'utf-8') +end + +---@param params nvim.CodeActionParamsResponse +function Action:generate_delegate_methods_prompt(params) + local status = self.jdtls:java_check_delegate_methods_status(params.params) + + if not status or not status.delegateFields or #status.delegateFields < 1 then + notify.warn('All delegatable methods are already implemented.') + return + end + + local selected_delegate_field = ui.select( + 'Select target to generate delegates for.', + status.delegateFields, + function(field) + return field.field.name .. ': ' .. field.field.type + end + ) + + if not selected_delegate_field then + return + end + + if #selected_delegate_field.delegateMethods < 1 then + notify.warn('All delegatable methods are already implemented.') + return + end + + local selected_delegate_methods = ui.multi_select( + 'Select methods to generate delegates for.', + selected_delegate_field.delegateMethods, + function(method) + return string.format( + '%s.%s(%s)', + selected_delegate_field.field.name, + method.name, + table.concat(method.parameters, ', ') + ) + end + ) + + if not selected_delegate_methods or #selected_delegate_methods < 1 then + return + end + + local delegate_entries = List:new(selected_delegate_methods):map( + ---@param method jdtls.MethodBinding + function(method) + return { + field = selected_delegate_field.field, + delegateMethod = method, + } + end + ) + + local edit = self.jdtls:java_generate_delegate_methods({ + context = params.params, + delegateEntries = delegate_entries, + }) + + vim.lsp.util.apply_workspace_edit(edit, 'utf-8') +end + +---@param command lsp.Command +function Action:apply_refactoring_command(command) + local action_name = command.arguments[1] --[[@as jdtls.CodeActionCommand]] + local action_context = command.arguments[2] --[[@as lsp.CodeActionParams]] + local action_info = command.arguments[3] --[[@as lsp.LSPAny]] + + self.refactor:refactor(action_name, action_context, action_info) +end + +---comment +---@param is_full_compile boolean +---@return java-core.CompileWorkspaceStatus +function Action:build_workspace(is_full_compile) + return self.jdtls:java_build_workspace(is_full_compile, 0) +end + +function Action:clean_workspace() + local client = lsp_utils.get_jdtls() + local data_path = lsp_utils.get_jdtls_cache_data_path(client.root_dir) + + local prompt = string.format('Do you want to delete "%s"', data_path) + + local choice = ui.select(prompt, { 'Yes', 'No' }) + + if choice ~= 'Yes' then + return + end + + return vim.fn.delete(data_path, 'rf') +end + +---@class java-refactor.ApplyRefactoringCommandParams +---@field bufnr number +---@field client_id number +---@field method string +---@field params lsp.CodeActionParams +---@field version number + +---@param params nvim.CodeActionParamsResponse +function Action:override_methods_prompt(params) + local status = self.jdtls:list_overridable_methods(params.params) + + if not status or not status.methods or #status.methods < 1 then + notify.warn('No methods to override.') + return + end + + local selected_methods = ui.multi_select('Select methods to override.', status.methods, function(method) + return string.format('%s(%s)', method.name, table.concat(method.parameters, ', ')) + end) + + if not selected_methods or #selected_methods < 1 then + return + end + + local edit = self.jdtls:add_overridable_methods(params.params, selected_methods) + vim.lsp.util.apply_workspace_edit(edit, 'utf-8') +end + +---@param selections jdtls.ImportSelection[] +function Action:choose_imports(selections) + local selected_candidates = {} + + for _, selection in ipairs(selections) do + local selected_candidate = ui.select_sync( + 'Select methods to override.', + selection.candidates, + function(candidate, index) + return index .. ' ' .. candidate.fullyQualifiedName + end + ) + + table.insert(selected_candidates, selected_candidate) + end + + return selected_candidates +end + +return Action diff --git a/lua/java-refactor/api/build.lua b/lua/java-refactor/api/build.lua new file mode 100644 index 0000000..60e8fcb --- /dev/null +++ b/lua/java-refactor/api/build.lua @@ -0,0 +1,19 @@ +---@param client_command jdtls.ClientCommand +local function run_client_command(client_command, ...) + local handlers = require('java-refactor.client-command-handlers') + handlers[client_command](...) +end + +local M = { + build_workspace = function() + local ClientCommand = require('java-refactor.client-command') + run_client_command(ClientCommand.COMPILE_WORKSPACE, true) + end, + + clean_workspace = function() + local ClientCommand = require('java-refactor.client-command') + run_client_command(ClientCommand.CLEAN_WORKSPACE) + end, +} + +return M diff --git a/lua/java-refactor/api/refactor.lua b/lua/java-refactor/api/refactor.lua new file mode 100644 index 0000000..2616243 --- /dev/null +++ b/lua/java-refactor/api/refactor.lua @@ -0,0 +1,38 @@ +---@param action_type string +---@param filter? string +local function run_code_action(action_type, filter) + vim.lsp.buf.code_action({ + apply = true, + context = { + diagnostics = vim.lsp.diagnostic.get_line_diagnostics(0), + only = { action_type }, + }, + filter = filter and function(refactor) + return refactor.command.arguments[1] == filter + end or nil, + }) +end + +local M = { + extract_variable = function() + run_code_action('refactor.extract.variable', 'extractVariable') + end, + + extract_variable_all_occurrence = function() + run_code_action('refactor.extract.variable', 'extractVariableAllOccurrence') + end, + + extract_constant = function() + run_code_action('refactor.extract.constant') + end, + + extract_method = function() + run_code_action('refactor.extract.function') + end, + + extract_field = function() + run_code_action('refactor.extract.field') + end, +} + +return M diff --git a/lua/java-refactor/client-command-handlers.lua b/lua/java-refactor/client-command-handlers.lua new file mode 100644 index 0000000..905a998 --- /dev/null +++ b/lua/java-refactor/client-command-handlers.lua @@ -0,0 +1,129 @@ +local ClientCommand = require('java-refactor.client-command') + +---@param message string +---@param func fun(action: java-refactor.Action) +local run = function(message, func) + local runner = require('async.runner') + local get_error_handler = require('java-refactor.utils.error_handler') + local instance = require('java-refactor.utils.instance-factory') + + runner(function() + func(instance.get_action()) + end) + .catch(get_error_handler(message)) + .run() +end + +local M = { + ---@param params java-refactor.RenameAction[] + [ClientCommand.RENAME_COMMAND] = function(params) + run('Failed to rename the symbol', function(action) + action.rename(params) + end) + end, + + ---@param params nvim.CodeActionParamsResponse + [ClientCommand.GENERATE_CONSTRUCTORS_PROMPT] = function(_, params) + run('Failed to generate constructor', function(action) + action:generate_constructor(params) + end) + end, + + ---@param params nvim.CodeActionParamsResponse + [ClientCommand.GENERATE_TOSTRING_PROMPT] = function(_, params) + run('Failed to generate toString', function(action) + action:generate_to_string(params) + end) + end, + + ---@param params nvim.CodeActionParamsResponse + [ClientCommand.HASHCODE_EQUALS_PROMPT] = function(_, params) + run('Failed to generate hash code and equals', function(action) + action:generate_hash_code_and_equals(params) + end) + end, + + ---@param params nvim.CodeActionParamsResponse + [ClientCommand.GENERATE_DELEGATE_METHODS_PROMPT] = function(_, params) + run('Failed to generate delegate methods', function(action) + action:generate_delegate_methods_prompt(params) + end) + end, + + ---@param command lsp.Command + [ClientCommand.APPLY_REFACTORING_COMMAND] = function(command) + run('Failed to apply refactoring command', function(action) + action:apply_refactoring_command(command) + end) + end, + + ---@param params nvim.CodeActionParamsResponse + [ClientCommand.OVERRIDE_METHODS_PROMPT] = function(_, params) + run('Failed to get overridable methods', function(action) + action:override_methods_prompt(params) + require('java-core.utils.notify').info('Successfully built the workspace') + end) + end, + + ---@param params [string, jdtls.ImportSelection[], boolean] + [ClientCommand.CHOOSE_IMPORTS] = function(params) + local get_error_handler = require('java-refactor.utils.error_handler') + local instance = require('java-refactor.utils.instance-factory') + local action = instance.get_action() + + local selections = params[2] + local ok, result = pcall(function() + return action.choose_imports(selections) + end) + + if not ok then + get_error_handler('Failed to choose imports')(result) + return + end + + return result or {} + end, + + ---@param is_full_build boolean + [ClientCommand.COMPILE_WORKSPACE] = function(is_full_build) + run('Failed to build workspace', function(action) + local notify = require('java-core.utils.notify') + + action:build_workspace(is_full_build) + notify.info('Successfully built the workspace') + end) + end, + + [ClientCommand.CLEAN_WORKSPACE] = function() + run('Failed to clean workspace', function(action) + local lsp_utils = require('java-core.utils.lsp') + + local result = action:clean_workspace() + if result == 0 then + lsp_utils.restart_ls('jdtls') + end + end) + end, +} + +local ignored_commands = { ClientCommand.REFRESH_BUNDLES_COMMAND } + +for _, command in pairs(ClientCommand) do + if not M[command] and not vim.tbl_contains(ignored_commands, command) then + local message = string.format( + '"%s" is not supported yet!' + .. '\nPlease request the feature using below link' + .. '\nhttps://github.com/nvim-java/nvim-java/issues/new?assignees=' + .. '&labels=enhancement&projects=&template=feature_request.yml&title=feature%%3A+', + command + ) + + M[command] = function() + require('java-core.utils.notify').warn(message) + + return vim.lsp.rpc_response_error(vim.lsp.protocol.ErrorCodes.MethodNotFound, 'Not implemented yet') + end + end +end + +return M diff --git a/lua/java-refactor/client-command.lua b/lua/java-refactor/client-command.lua new file mode 100644 index 0000000..aadd352 --- /dev/null +++ b/lua/java-refactor/client-command.lua @@ -0,0 +1,93 @@ +---@enum jdtls.ClientCommand +local M = { + ADD_TO_SOURCEPATH = 'java.project.addToSourcePath', + ADD_TO_SOURCEPATH_CMD = 'java.project.addToSourcePath.command', + APPLY_REFACTORING_COMMAND = 'java.action.applyRefactoringCommand', + APPLY_WORKSPACE_EDIT = 'java.apply.workspaceEdit', + BUILD_PROJECT = 'java.project.build', + CHANGE_BASE_TYPE = 'java.action.changeBaseType', + CHANGE_IMPORTED_PROJECTS = 'java.project.changeImportedProjects', + CHOOSE_IMPORTS = 'java.action.organizeImports.chooseImports', + CLEAN_SHARED_INDEXES = 'java.clean.sharedIndexes', + CLEAN_WORKSPACE = 'java.clean.workspace', + CLIPBOARD_ONPASTE = 'java.action.clipboardPasteAction', + COMPILE_WORKSPACE = 'java.workspace.compile', + CONFIGURATION_UPDATE = 'java.projectConfiguration.update', + CREATE_MODULE_INFO = 'java.project.createModuleInfo', + CREATE_MODULE_INFO_COMMAND = 'java.project.createModuleInfo.command', + EXECUTE_WORKSPACE_COMMAND = 'java.execute.workspaceCommand', + FILESEXPLORER_ONPASTE = 'java.action.filesExplorerPasteAction', + GENERATE_ACCESSORS_PROMPT = 'java.action.generateAccessorsPrompt', + GENERATE_CONSTRUCTORS_PROMPT = 'java.action.generateConstructorsPrompt', + GENERATE_DELEGATE_METHODS_PROMPT = 'java.action.generateDelegateMethodsPrompt', + GENERATE_TOSTRING_PROMPT = 'java.action.generateToStringPrompt', + GET_ALL_JAVA_PROJECTS = 'java.project.getAll', + GET_CLASSPATHS = 'java.project.getClasspaths', + GET_DECOMPILED_SOURCE = 'java.decompile', + GET_PROJECT_SETTINGS = 'java.project.getSettings', + GET_WORKSPACE_PATH = '_java.workspace.path', + GOTO_LOCATION = 'editor.action.goToLocations', + HANDLE_PASTE_EVENT = 'java.edit.handlePasteEvent', + HASHCODE_EQUALS_PROMPT = 'java.action.hashCodeEqualsPrompt', + IGNORE_INCOMPLETE_CLASSPATH = 'java.ignoreIncompleteClasspath', + IGNORE_INCOMPLETE_CLASSPATH_HELP = 'java.ignoreIncompleteClasspath.help', + IMPORT_PROJECTS = 'java.project.import', + IMPORT_PROJECTS_CMD = 'java.project.import.command', + IS_TEST_FILE = 'java.project.isTestFile', + LEARN_MORE_ABOUT_CLEAN_UPS = '_java.learnMoreAboutCleanUps', + LEARN_MORE_ABOUT_REFACTORING = '_java.learnMoreAboutRefactorings', + LIST_SOURCEPATHS = 'java.project.listSourcePaths', + LIST_SOURCEPATHS_CMD = 'java.project.listSourcePaths.command', + LOMBOK_CONFIGURE = 'java.lombokConfigure', + MANUAL_CLEANUP = 'java.action.doCleanup', + MARKDOWN_API_RENDER = 'markdown.api.render', + METADATA_FILES_GENERATION = '_java.metadataFilesGeneration', + NAVIGATE_TO_SUPER_IMPLEMENTATION_COMMAND = 'java.action.navigateToSuperImplementation', + NOT_COVERED_EXECUTION = '_java.notCoveredExecution', + NULL_ANALYSIS_SET_MODE = 'java.compile.nullAnalysis.setMode', + OPEN_BROWSER = 'vscode.open', + OPEN_CLIENT_LOG = 'java.open.clientLog', + OPEN_FILE = 'java.open.file', + OPEN_FORMATTER = 'java.open.formatter.settings', + OPEN_JSON_SETTINGS = 'workbench.action.openSettingsJson', + OPEN_LOGS = 'java.open.logs', + OPEN_OUTPUT = 'java.open.output', + OPEN_SERVER_LOG = 'java.open.serverLog', + OPEN_SERVER_STDERR_LOG = 'java.open.serverStderrLog', + OPEN_SERVER_STDOUT_LOG = 'java.open.serverStdoutLog', + OPEN_STATUS_SHORTCUT = '_java.openShortcuts', + OPEN_TYPE_HIERARCHY = 'java.navigate.openTypeHierarchy', + ORGANIZE_IMPORTS = 'java.action.organizeImports', + ORGANIZE_IMPORTS_SILENTLY = 'java.edit.organizeImports', + OVERRIDE_METHODS_PROMPT = 'java.action.overrideMethodsPrompt', + PROJECT_CONFIGURATION_STATUS = 'java.projectConfiguration.status', + REFRESH_BUNDLES = 'java.reloadBundles', + REFRESH_BUNDLES_COMMAND = '_java.reloadBundles.command', + RELOAD_WINDOW = 'workbench.action.reloadWindow', + REMOVE_FROM_SOURCEPATH = 'java.project.removeFromSourcePath', + REMOVE_FROM_SOURCEPATH_CMD = 'java.project.removeFromSourcePath.command', + RENAME_COMMAND = 'java.action.rename', + RESOLVE_PASTED_TEXT = 'java.project.resolveText', + RESOLVE_SOURCE_ATTACHMENT = 'java.project.resolveSourceAttachment', + RESOLVE_TYPE_HIERARCHY = 'java.navigate.resolveTypeHierarchy', + RESOLVE_WORKSPACE_SYMBOL = 'java.project.resolveWorkspaceSymbol', + RESTART_LANGUAGE_SERVER = 'java.server.restart', + RUNTIME_VALIDATION_OPEN = 'java.runtimeValidation.open', + SHOW_CLASS_HIERARCHY = 'java.action.showClassHierarchy', + SHOW_JAVA_IMPLEMENTATIONS = 'java.show.implementations', + SHOW_JAVA_REFERENCES = 'java.show.references', + SHOW_REFERENCES = 'editor.action.showReferences', + SHOW_SERVER_TASK_STATUS = 'java.show.server.task.status', + SHOW_SUBTYPE_HIERARCHY = 'java.action.showSubtypeHierarchy', + SHOW_SUPERTYPE_HIERARCHY = 'java.action.showSupertypeHierarchy', + SHOW_TYPE_HIERARCHY = 'java.action.showTypeHierarchy', + SMARTSEMICOLON_DETECTION = 'java.edit.smartSemicolonDetection', + SWITCH_SERVER_MODE = 'java.server.mode.switch', + TEMPLATE_VARIABLES = '_java.templateVariables', + UPDATE_SOURCE_ATTACHMENT = 'java.project.updateSourceAttachment', + UPDATE_SOURCE_ATTACHMENT_CMD = 'java.project.updateSourceAttachment.command', + UPGRADE_GRADLE_WRAPPER = 'java.project.upgradeGradle', + UPGRADE_GRADLE_WRAPPER_CMD = 'java.project.upgradeGradle.command', +} + +return M diff --git a/lua/java-refactor/init.lua b/lua/java-refactor/init.lua new file mode 100644 index 0000000..60655da --- /dev/null +++ b/lua/java-refactor/init.lua @@ -0,0 +1,46 @@ +local event = require('java-core.utils.event') +local cmd_util = require('java-core.utils.command') + +local M = {} + +local group = vim.api.nvim_create_augroup('java-refactor-command-register', {}) + +M.setup = function() + event.on_jdtls_attach({ + group = group, + once = true, + callback = function() + M.reg_client_commands() + M.reg_refactor_commands() + M.reg_build_commands() + end, + }) +end + +M.reg_client_commands = function() + local code_action_handlers = require('java-refactor.client-command-handlers') + + for key, handler in pairs(code_action_handlers) do + vim.lsp.commands[key] = handler + end +end + +M.reg_refactor_commands = function() + local java = require('java') + local code_action_api = require('java-refactor.api.refactor') + + for api_name, api in pairs(code_action_api) do + cmd_util.register_api(java, { 'refactor', api_name }, api, { range = 2 }) + end +end + +M.reg_build_commands = function() + local java = require('java') + local code_action_api = require('java-refactor.api.build') + + for api_name, api in pairs(code_action_api) do + cmd_util.register_api(java, { 'build', api_name }, api, {}) + end +end + +return M diff --git a/lua/java-refactor/refactor.lua b/lua/java-refactor/refactor.lua new file mode 100644 index 0000000..6b2f731 --- /dev/null +++ b/lua/java-refactor/refactor.lua @@ -0,0 +1,359 @@ +local class = require('java-core.utils.class') +local notify = require('java-core.utils.notify') +local JdtlsClient = require('java-core.ls.clients.jdtls-client') +local List = require('java-core.utils.list') +local ui = require('java.ui.utils') + +local refactor_edit_request_needed_actions = { + 'convertVariableToField', + 'extractConstant', + 'extractField', + 'extractMethod', + 'extractVariable', + 'extractVariableAllOccurrence', +} + +local available_actions = List:new({ + 'assignField', + 'assignVariable', + 'convertAnonymousClassToNestedCommand', + 'introduceParameter', + 'invertVariable', + -- 'moveFile', + 'moveInstanceMethod', + 'moveStaticMember', + 'moveType', + -- 'changeSignature', + -- 'extractInterface', +}):concat(refactor_edit_request_needed_actions) + +---@class java-refactor.Refactor +---@field jdtls_client java-core.JdtlsClient +local Refactor = class() + +---@param client vim.lsp.Client +function Refactor:_init(client) + self.jdtls_client = JdtlsClient(client) +end + +---Run refactor command +---@param action_name jdtls.CodeActionCommand +---@param action_context lsp.CodeActionParams +---@param action_info lsp.LSPAny +function Refactor:refactor(action_name, action_context, action_info) + if not vim.tbl_contains(available_actions, action_name) then + notify.error(string.format('Refactoring command "%s" is not supported', action_name)) + return + end + + if vim.tbl_contains(refactor_edit_request_needed_actions, action_name) then + local formatting_options = self:make_formatting_options() + local selections + + if vim.tbl_contains(refactor_edit_request_needed_actions, action_name) then + selections = self:get_selections(action_name, action_context) + end + + local changes = self.jdtls_client:java_get_refactor_edit( + action_name, + action_context, + formatting_options, + selections, + vim.api.nvim_get_current_buf() + ) + + self:perform_refactor_edit(changes) + elseif action_name == 'moveFile' then + self:move_file(action_info --[[@as jdtls.CodeActionMoveTypeCommandInfo]]) + elseif action_name == 'moveType' then + self:move_type(action_context, action_info --[[@as jdtls.CodeActionMoveTypeCommandInfo]]) + elseif action_name == 'moveStaticMember' then + self:move_static_member(action_context, action_info --[[@as jdtls.CodeActionMoveTypeCommandInfo]]) + elseif action_name == 'moveInstanceMethod' then + self:move_instance_method(action_context, action_info --[[@as jdtls.CodeActionMoveTypeCommandInfo]]) + end +end + +---@private +---@param action_info jdtls.CodeActionMoveTypeCommandInfo +function Refactor:move_file(action_info) + if not action_info or not action_info.uri then + return + end + + local move_des = self.jdtls_client:get_move_destination({ + moveKind = 'moveResource', + sourceUris = { action_info.uri }, + params = nil, + }) + + if not move_des or not move_des.destinations or #move_des.destinations < 1 then + notify.error('Cannot find available Java packages to move the selected files to.') + return + end + + ---@type jdtls.ResourceMoveDestination[] + local destinations = move_des.destinations + + local selected_destination = ui.select('Choose the target package', destinations, function(destination) + return destination.displayName .. ' ' .. destination.path + end) + + if not selected_destination then + return + end + + local changes = self.jdtls_client:java_move({ + moveKind = 'moveResource', + sourceUris = { action_info.uri }, + params = nil, + destination = selected_destination, + }) + + self:perform_refactor_edit(changes) +end + +---@private +---@param action_context lsp.CodeActionParams +---@param action_info jdtls.CodeActionMoveTypeCommandInfo +function Refactor:move_instance_method(action_context, action_info) + local move_des = self.jdtls_client:get_move_destination({ + moveKind = 'moveInstanceMethod', + sourceUris = { action_context.textDocument.uri }, + params = action_context, + }) + + if move_des and move_des.errorMessage then + notify.error(move_des.errorMessage) + return + end + + if not move_des or not move_des.destinations or #move_des.destinations < 1 then + notify.error('Cannot find possible class targets to move the selected method to.') + return + end + + ---@type jdtls.InstanceMethodMoveDestination[] + local destinations = move_des.destinations + + local method_name = action_info and action_info.displayName or '' + + local selected_destination = ui.select( + string.format('Select the new class for the instance method %s', method_name), + destinations, + function(destination) + return destination.type .. ' ' .. destination.name + end, + { prompt_single = true } + ) + + if not selected_destination then + return + end + + self:perform_move('moveInstanceMethod', action_context, selected_destination) +end + +---@private +---@param action_context lsp.CodeActionParams +---@param action_info jdtls.CodeActionMoveTypeCommandInfo +function Refactor:move_static_member(action_context, action_info) + local exclude = List:new() + + if action_info.enclosingTypeName then + exclude:push(action_info.enclosingTypeName) + if action_info.memberType == 55 or action_info.memberType == 71 or action_info.memberType == 81 then + exclude:push(action_info.enclosingTypeName .. '.' .. action_info.displayName) + end + end + + local project_name = action_info and action_info.projectName or nil + local member_name = action_info and action_info.displayName and action_info.displayName or '' + + local selected_class = self:select_target_class( + string.format('Select the new class for the static member %s.', member_name), + project_name, + exclude + ) + + if not selected_class then + return + end + + self:perform_move('moveStaticMember', action_context, selected_class) +end + +---@private +---@param action_context lsp.CodeActionParams +---@param action_info jdtls.CodeActionMoveTypeCommandInfo +function Refactor:move_type(action_context, action_info) + if not action_info or not action_info.supportedDestinationKinds then + return + end + + local selected_destination_kind = ui.select( + 'What would you like to do?', + action_info.supportedDestinationKinds, + function(kind) + if kind == 'newFile' then + return string.format('Move type "%s" to new file', action_info.displayName) + else + return string.format('Move type "%s" to another class', action_info.displayName) + end + end + ) + + if not selected_destination_kind then + return + end + + if selected_destination_kind == 'newFile' then + self:perform_move('moveTypeToNewFile', action_context) + else + local exclude = List:new() + + if action_info.enclosingTypeName then + exclude:push(action_info.enclosingTypeName) + exclude:push(action_info.enclosingTypeName .. ':' .. action_info.displayName) + end + + local selected_class = self:select_target_class( + string.format('Select the new class for the type %s.', action_info.displayName), + action_info.projectName, + exclude + ) + + if not selected_class then + return + end + + self:perform_move('moveStaticMember', action_context, selected_class) + end +end + +---@private +---@param move_kind string +---@param action_context lsp.CodeActionParams +---@param destination? jdtls.InstanceMethodMoveDestination | jdtls.ResourceMoveDestination | lsp.SymbolInformation +function Refactor:perform_move(move_kind, action_context, destination) + local changes = self.jdtls_client:java_move({ + moveKind = move_kind, + sourceUris = { action_context.textDocument.uri }, + params = action_context, + destination = destination, + }) + + self:perform_refactor_edit(changes) +end + +---@private +---@param changes jdtls.RefactorWorkspaceEdit +function Refactor:perform_refactor_edit(changes) + if not changes then + notify.warn('No edits suggested for the code action') + return + end + + if changes.errorMessage then + notify.error(changes.errorMessage) + return + end + vim.lsp.util.apply_workspace_edit(changes.edit, 'utf-8') + + if changes.command then + self:run_lsp_client_command(changes.command.command, changes.command.arguments) + end +end + +---@private +---@param prompt string +---@param project_name string +---@param exclude string[] +function Refactor:select_target_class(prompt, project_name, exclude) + local classes = self.jdtls_client:java_search_symbols({ + query = '*', + projectName = project_name, + sourceOnly = true, + }) + + ---@type lsp.SymbolInformation[] + local filtered_classes = List:new(classes):filter(function(cls) + local type_name = cls.containerName .. '.' .. cls.name + return not vim.tbl_contains(exclude, type_name) + end) + + local selected = ui.select(prompt, filtered_classes, function(cls) + return cls.containerName .. '.' .. cls.name + end) + + return selected +end + +---@private +---@param command_name string +---@param arguments any +function Refactor:run_lsp_client_command(command_name, arguments) + local command = vim.lsp.commands[command_name] + + if not command then + notify.error('Command "' .. command_name .. '" is not supported') + return + end + + command(arguments) +end + +---@private +---@return lsp.FormattingOptions +function Refactor:make_formatting_options() + return { + tabSize = vim.bo.tabstop, + insertSpaces = vim.bo.expandtab, + } +end + +---@private +---@param refactor_type jdtls.CodeActionCommand +---@param params lsp.CodeActionParams +---@return jdtls.SelectionInfo[] +function Refactor:get_selections(refactor_type, params) + local selections = List:new() + local buffer = vim.api.nvim_get_current_buf() + + if + params.range.start.character == params.range['end'].character + and params.range.start.line == params.range['end'].line + then + local selection_res = self.jdtls_client:java_infer_selection(refactor_type, params, buffer) + + if not selection_res then + return selections + end + + local selection = selection_res[1] + + if selection.params and vim.islist(selection.params) then + local initialize_in = ui.select('Initialize the field in', selection.params) + + if not initialize_in then + return selections + end + + selections:push(initialize_in) + end + + selections:push(selection) + end + + return selections +end + +---@class jdtls.CodeActionMoveTypeCommandInfo +---@field displayName string +---@field enclosingTypeName string +---@field memberType number +---@field projectName string +---@field supportedDestinationKinds string[] +---@field uri? string + +return Refactor diff --git a/lua/java-refactor/utils/error_handler.lua b/lua/java-refactor/utils/error_handler.lua new file mode 100644 index 0000000..a09ed73 --- /dev/null +++ b/lua/java-refactor/utils/error_handler.lua @@ -0,0 +1,9 @@ +local get_error_handler = require('java-core.utils.error_handler') +local log = require('java-core.utils.log2') + +---Returns a error handler +---@param msg string messages to show in the error +---@return fun(err: any) # function that log and notify the error +return function(msg) + return get_error_handler(msg, log) +end diff --git a/lua/java-refactor/utils/instance-factory.lua b/lua/java-refactor/utils/instance-factory.lua new file mode 100644 index 0000000..742a4dd --- /dev/null +++ b/lua/java-refactor/utils/instance-factory.lua @@ -0,0 +1,12 @@ +local M = {} + +---@return java-refactor.Action +function M.get_action() + local lsp_utils = require('java-core.utils.lsp') + local Action = require('java-refactor.action') + local client = lsp_utils.get_jdtls() + + return Action(client) +end + +return M diff --git a/lua/java/runner/run-logger.lua b/lua/java-runner/run-logger.lua similarity index 100% rename from lua/java/runner/run-logger.lua rename to lua/java-runner/run-logger.lua diff --git a/lua/java/runner/run.lua b/lua/java-runner/run.lua similarity index 94% rename from lua/java/runner/run.lua rename to lua/java-runner/run.lua index 1691823..c110589 100644 --- a/lua/java/runner/run.lua +++ b/lua/java-runner/run.lua @@ -13,7 +13,6 @@ local notify = require('java-core.utils.notify') local Run = class() ---@param dap_config java-dap.DapLauncherConfig ----@param cmd string[] function Run:_init(dap_config) self.name = dap_config.name self.main_class = dap_config.mainClass @@ -53,8 +52,8 @@ function Run:stop() self.job_chan_id = nil end ----Send data to execution job channel ---@private +---Send data to execution job channel ---@param data string function Run:send_job(data) if self.job_chan_id then @@ -62,19 +61,18 @@ function Run:send_job(data) end end ----Send message to terminal channel ---@private +---Send message to terminal channel ---@param data string function Run:send_term(data) vim.fn.chansend(self.term_chan_id, data) end ----Runs when the current job exists ---@private +---Runs when the current job exists ---@param exit_code number function Run:on_job_exit(exit_code) - local message = - string.format('Process finished with exit code::%s', exit_code) + local message = string.format('Process finished with exit code::%s', exit_code) self:send_term(message) self.is_running = false diff --git a/lua/java/runner/runner.lua b/lua/java-runner/runner.lua similarity index 84% rename from lua/java/runner/runner.lua rename to lua/java-runner/runner.lua index 1120224..bce939e 100644 --- a/lua/java/runner/runner.lua +++ b/lua/java-runner/runner.lua @@ -1,10 +1,10 @@ -local ui = require('java.utils.ui') +local ui = require('java.ui.utils') local class = require('java-core.utils.class') -local jdtls = require('java.utils.jdtls2') +local lsp_utils = require('java-core.utils.lsp') local profile_config = require('java.api.profile_config') -local Run = require('java.runner.run') -local RunLogger = require('java.runner.run-logger') -local DapSetup = require('java-dap.api.setup') +local Run = require('java-runner.run') +local RunLogger = require('java-runner.run-logger') +local DapSetup = require('java-dap.setup') ---@class java.Runner ---@field runs table @@ -20,7 +20,7 @@ end ---Starts a new run ---@param args string function Runner:start_run(args) - local cmd, dap_config = Runner.select_dap_config(args) + local cmd, dap_config = self:select_dap_config(args) if not cmd or not dap_config then return @@ -100,17 +100,13 @@ end ---@param args string additional program arguments to pass ---@return string[] | nil ---@return java-dap.DapLauncherConfig | nil -function Runner.select_dap_config(args) - local dap = DapSetup(jdtls()) +function Runner:select_dap_config(args) + local dap = DapSetup(lsp_utils.get_jdtls()) local dap_config_list = dap:get_dap_config() - local selected_dap_config = ui.select( - 'Select the main class (module -> mainClass)', - dap_config_list, - function(config) - return config.name - end - ) + local selected_dap_config = ui.select('Select the main class (module -> mainClass)', dap_config_list, function(config) + return config.name + end) if not selected_dap_config then return nil, nil diff --git a/lua/java-test/adapters.lua b/lua/java-test/adapters.lua new file mode 100644 index 0000000..ed10d1e --- /dev/null +++ b/lua/java-test/adapters.lua @@ -0,0 +1,42 @@ +local List = require('java-core.utils.list') +local JavaTestClient = require('java-core.ls.clients.java-test-client') + +local M = {} + +---Returns launch argument parameters for given test or tests +---@param tests java-core.TestDetails | java-core.TestDetails[] +---@return java-core.JavaCoreTestResolveJUnitLaunchArgumentsParams junit launch arguments +function M.tests_to_junit_launch_params(tests) + if not vim.islist(tests) then + return { + projectName = tests.projectName, + testLevel = tests.testLevel, + testKind = tests.testKind, + testNames = M.get_test_names({ tests }), + } + end + + local first_test = tests[1] + + return { + projectName = first_test.projectName, + testLevel = first_test.testLevel, + testKind = first_test.testKind, + testNames = M.get_test_names(tests), + } +end + +---Returns a list of test names to be passed to test launch arguments resolver +---@param tests java-core.TestDetails[] +---@return java-core.List +function M.get_test_names(tests) + return List:new(tests):map(function(test) + if test.testKind == JavaTestClient.TestKind.TestNG or test.testLevel == JavaTestClient.TestLevel.Class then + return test.fullName + end + + return test.jdtHandler + end) +end + +return M diff --git a/lua/java-test/api.lua b/lua/java-test/api.lua new file mode 100644 index 0000000..e4879fd --- /dev/null +++ b/lua/java-test/api.lua @@ -0,0 +1,142 @@ +local log = require('java-core.utils.log2') +local notify = require('java-core.utils.notify') +local test_adapters = require('java-test.adapters') +local dap_adapters = require('java-dap.data-adapters') +local buf_util = require('java-core.utils.buffer') +local win_util = require('java.utils.window') + +local DebugClient = require('java-core.ls.clients.java-debug-client') +local TestClient = require('java-core.ls.clients.java-test-client') + +---@class java_test.TestApi +---@field private client java-core.JdtlsClient +---@field private debug_client java-core.DebugClient +---@field private test_client java-core.TestClient +---@field private runner java-dap.DapRunner +local M = {} + +---Returns a new test helper client +---@param args { client: vim.lsp.Client, runner: java-dap.DapRunner } +---@return java_test.TestApi +function M:new(args) + local o = { + client = args.client, + } + + o.debug_client = DebugClient(args.client) + o.test_client = TestClient(args.client) + o.runner = args.runner + + setmetatable(o, self) + self.__index = self + + return o +end + +---Returns a list of test methods +---@param file_uri string uri of the class +---@return java-core.TestDetailsWithRange[] # list of test methods +function M:get_test_methods(file_uri) + local classes = self.test_client:find_test_types_and_methods(file_uri) + local methods = {} + + for _, class in ipairs(classes) do + for _, method in ipairs(class.children) do + ---@diagnostic disable-next-line: inject-field + method.class = class + table.insert(methods, method) + end + end + + return methods +end + +---comment +---@param buffer number +---@param report java-test.JUnitTestReport +---@param config? java-dap.DapLauncherConfigOverridable config to override the default values in test launcher config +function M:run_class_by_buffer(buffer, report, config) + local tests = self:get_test_class_by_buffer(buffer) + + if #tests < 1 then + notify.warn('No tests found in the current buffer') + return + end + + self:run_test(tests, report, config) +end + +---Returns test classes in the given buffer +---@private +---@param buffer integer +---@return java-core.TestDetailsWithChildrenAndRange # get test class details +function M:get_test_class_by_buffer(buffer) + log.debug('finding test class by buffer') + + local uri = vim.uri_from_bufnr(buffer) + return self.test_client:find_test_types_and_methods(uri) +end + +---Run the given test +---@param tests java-core.TestDetails[] +---@param report java-test.JUnitTestReport +---@param config? java-dap.DapLauncherConfigOverridable config to override the default values in test launcher config +function M:run_test(tests, report, config) + local launch_args = self.test_client:resolve_junit_launch_arguments(test_adapters.tests_to_junit_launch_params(tests)) + + local java_exec = self.debug_client:resolve_java_executable(launch_args.mainClass, launch_args.projectName) + + local dap_launcher_config = dap_adapters.junit_launch_args_to_dap_config(launch_args, java_exec, { + debug = true, + label = 'Launch All Java Tests', + }) + + dap_launcher_config = vim.tbl_deep_extend('force', dap_launcher_config, config or {}) + + self.runner:run_by_config(dap_launcher_config, report) +end + +---Run the current test class +---@param report java-test.JUnitTestReport +---@param config java-dap.DapLauncherConfigOverridable +function M:execute_current_test_class(report, config) + log.debug('running the current class') + + return self:run_class_by_buffer(buf_util.get_curr_buf(), report, config) +end + +---Run the current test method +---@param report java-test.JUnitTestReport +---@param config java-dap.DapLauncherConfigOverridable +function M:execute_current_test_method(report, config) + log.debug('running the current method') + + local method = self:find_current_test_method() + + if not method then + notify.warn('cursor is not on a test method') + return + end + + self:run_test({ method }, report, config) +end + +---Find the test method at the current cursor position +---@return java-core.TestDetailsWithRange | nil +function M:find_current_test_method() + log.debug('finding the current test method') + + local cursor = win_util.get_cursor() + local methods = self:get_test_methods(buf_util.get_curr_uri()) + + for _, method in ipairs(methods) do + local line_start = method.range.start.line + local line_end = method.range['end'].line + + if cursor.line >= line_start and cursor.line <= line_end then + return method + end + end +end + +return M diff --git a/lua/java-test/init.lua b/lua/java-test/init.lua new file mode 100644 index 0000000..2b2f481 --- /dev/null +++ b/lua/java-test/init.lua @@ -0,0 +1,87 @@ +local log = require('java-core.utils.log2') +local lsp_utils = require('java-core.utils.lsp') +local get_error_handler = require('java-core.utils.error_handler') + +local runner = require('async.runner') + +local JavaTestApi = require('java-test.api') +local DapRunner = require('java-dap.runner') +local JUnitReport = require('java-test.reports.junit') +local ResultParserFactory = require('java-test.results.result-parser-factory') +local ReportViewer = require('java-test.ui.floating-report-viewer') + +local M = { + ---@type java-test.JUnitTestReport + last_report = nil, +} + +function M.run_current_class() + log.info('run current test class') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + return test_api:execute_current_test_class(M.get_report(), { noDebug = true }) + end) + .catch(get_error_handler('failed to run the current test class')) + .run() +end + +function M.debug_current_class() + log.info('debug current test class') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + test_api:execute_current_test_class(M.get_report(), {}) + end) + .catch(get_error_handler('failed to debug the current test class')) + .run() +end + +function M.debug_current_method() + log.info('debug current test method') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + return test_api:execute_current_test_method(M.get_report(), {}) + end) + .catch(get_error_handler('failed to run the current test method')) + .run() +end + +function M.run_current_method() + log.info('run current test method') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + return test_api:execute_current_test_method(M.get_report(), { noDebug = true }) + end) + .catch(get_error_handler('failed to run the current test method')) + .run() +end + +function M.view_last_report() + if M.last_report then + M.last_report:show_report() + end +end + +---@private +function M.get_report() + local report = JUnitReport(ResultParserFactory(), ReportViewer()) + M.last_report = report + return report +end + +return M diff --git a/lua/java-test/reports/junit.lua b/lua/java-test/reports/junit.lua new file mode 100644 index 0000000..01edb1e --- /dev/null +++ b/lua/java-test/reports/junit.lua @@ -0,0 +1,73 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-test.JUnitTestReport +---@field private conn uv_tcp_t +---@field private result_parser java-test.TestParser +---@field private result_parser_fac java-test.TestParserFactory +---@field private report_viewer java-test.ReportViewer +---@overload fun(result_parser_factory: java-test.TestParserFactory, test_viewer: java-test.ReportViewer) +local JUnitReport = class() + +---Init +---@param result_parser_factory java-test.TestParserFactory +function JUnitReport:_init(result_parser_factory, report_viewer) + self.conn = nil + self.result_parser_fac = result_parser_factory + self.report_viewer = report_viewer +end + +---Returns the test results +---@return java-test.TestResults[] +function JUnitReport:get_results() + return self.result_parser:get_test_details() +end + +---Shows the test report +function JUnitReport:show_report() + self.report_viewer:show(self:get_results()) +end + +---Returns a stream reader function +---@param conn uv_tcp_t +---@return fun(err: string, buffer: string) # callback function +function JUnitReport:get_stream_reader(conn) + self.conn = conn + self.result_parser = self.result_parser_fac:get_parser() + + return vim.schedule_wrap(function(err, buffer) + if err then + self:on_error(err) + self:on_close() + self.conn:close() + return + end + + if buffer then + self:on_update(buffer) + else + self:on_close() + self.conn:close() + end + end) +end + +---Runs on connection update +---@private +---@param text string +function JUnitReport:on_update(text) + self.result_parser:parse(text) +end + +---Runs on connection close +---@private +function JUnitReport:on_close() end + +---Runs on connection error +---@private +---@param err string error +function JUnitReport:on_error(err) + log.error('Error while running test', err) +end + +return JUnitReport diff --git a/lua/java-test/results/execution-status.lua b/lua/java-test/results/execution-status.lua new file mode 100644 index 0000000..18008c0 --- /dev/null +++ b/lua/java-test/results/execution-status.lua @@ -0,0 +1,7 @@ +---@enum java-test.TestExecutionStatus +local TestStatus = { + Started = 'started', + Ended = 'ended', +} + +return TestStatus diff --git a/lua/java-test/results/message-id.lua b/lua/java-test/results/message-id.lua new file mode 100644 index 0000000..28a37a5 --- /dev/null +++ b/lua/java-test/results/message-id.lua @@ -0,0 +1,30 @@ +---@enum MessageId +local MessageId = { + -- Notification about a test inside the test suite. + -- TEST_TREE + testId + "," + testName + "," + isSuite + "," + testCount + "," + isDynamicTest + + -- "," + parentId + "," + displayName + "," + parameterTypes + "," + uniqueId + + -- isSuite = "true" or "false" + -- isDynamicTest = "true" or "false" + -- parentId = the unique id of its parent if it is a dynamic test, otherwise can be "-1" + -- displayName = the display name of the test + -- parameterTypes = comma-separated list of method parameter types if applicable, otherwise an + -- empty string + -- uniqueId = the unique ID of the test provided by JUnit launcher, otherwise an empty string + + TestTree = '%TSTTREE', + TestStart = '%TESTS', + TestEnd = '%TESTE', + TestFailed = '%FAILED', + TestError = '%ERROR', + ExpectStart = '%EXPECTS', + ExpectEnd = '%EXPECTE', + ActualStart = '%ACTUALS', + ActualEnd = '%ACTUALE', + TraceStart = '%TRACES', + TraceEnd = '%TRACEE', + IGNORE_TEST_PREFIX = '@Ignore: ', + ASSUMPTION_FAILED_TEST_PREFIX = '@AssumptionFailure: ', +} + +return MessageId diff --git a/lua/java-test/results/result-parser-factory.lua b/lua/java-test/results/result-parser-factory.lua new file mode 100644 index 0000000..4d635f8 --- /dev/null +++ b/lua/java-test/results/result-parser-factory.lua @@ -0,0 +1,13 @@ +local class = require('java-core.utils.class') +local TestParser = require('java-test.results.result-parser') + +---@class java-test.TestParserFactory +local TestParserFactory = class() + +---Returns a test parser of given type +---@return java-test.TestParser +function TestParserFactory:get_parser() + return TestParser() +end + +return TestParserFactory diff --git a/lua/java-test/results/result-parser.lua b/lua/java-test/results/result-parser.lua new file mode 100644 index 0000000..00468fc --- /dev/null +++ b/lua/java-test/results/result-parser.lua @@ -0,0 +1,196 @@ +local class = require('java-core.utils.class') + +local MessageId = require('java-test.results.message-id') +local TestStatus = require('java-test.results.result-status') +local TestExecStatus = require('java-test.results.execution-status') + +---@class java-test.TestParser +---@field private test_details java-test.TestResults[] +local TestParser = class() + +---Init +function TestParser:_init() + self.test_details = {} +end + +---@private +TestParser.node_parsers = { + [MessageId.TestTree] = 'parse_test_tree', + [MessageId.TestStart] = 'parse_test_start', + [MessageId.TestEnd] = 'parse_test_end', + [MessageId.TestFailed] = 'parse_test_failed', +} + +---@private +TestParser.strtobool = { + ['true'] = true, + ['false'] = false, +} + +---Parse a given text into test details +---@param text string test result buffer +function TestParser:parse(text) + if text:sub(-1) ~= '\n' then + text = text .. '\n' + end + + local line_iter = text:gmatch('(.-)\n') + + local line = line_iter() + + while line ~= nil do + local message_id = line:sub(1, 8):gsub('%s+', '') + local content = line:sub(9) + + local node_parser = TestParser.node_parsers[message_id] + + if node_parser then + local data = vim.split(content, ',', { plain = true, trimempty = true }) + + if self[TestParser.node_parsers[message_id]] then + self[TestParser.node_parsers[message_id]](self, data, line_iter) + end + end + + line = line_iter() + end +end + +---Returns the parsed test details +---@return java-test.TestResults # parsed test details +function TestParser:get_test_details() + return self.test_details +end + +---@private +function TestParser:parse_test_tree(data) + local node = { + test_id = tonumber(data[1]), + test_name = data[2], + is_suite = TestParser.strtobool[data[3]], + test_count = tonumber(data[4]), + is_dynamic_test = TestParser.strtobool[data[5]], + parent_id = tonumber(data[6]), + display_name = data[7], + parameter_types = data[8], + unique_id = data[9], + } + + local parent = self:find_result_node(node.parent_id) + + if not parent then + table.insert(self.test_details, node) + else + parent.children = parent.children or {} + table.insert(parent.children, node) + end +end + +---@private +function TestParser:parse_test_start(data) + local test_id = tonumber(data[1]) + local node = self:find_result_node(test_id) + assert(node) + node.result = {} + node.result.execution = TestExecStatus.Started +end + +---@private +function TestParser:parse_test_end(data) + local test_id = tonumber(data[1]) + local node = self:find_result_node(test_id) + assert(node) + node.result.execution = TestExecStatus.Ended +end + +---@private +function TestParser:parse_test_failed(data, line_iter) + local test_id = tonumber(data[1]) + local node = self:find_result_node(test_id) + assert(node) + + node.result.status = TestStatus.Failed + + while true do + local line = line_iter() + + if line == nil then + break + end + + -- EXPECTED + if vim.startswith(line, MessageId.ExpectStart) then + node.result.expected = self:get_content_until_end_tag(MessageId.ExpectEnd, line_iter) + + -- ACTUAL + elseif vim.startswith(line, MessageId.ActualStart) then + node.result.actual = self:get_content_until_end_tag(MessageId.ActualEnd, line_iter) + + -- TRACE + elseif vim.startswith(line, MessageId.TraceStart) then + node.result.trace = self:get_content_until_end_tag(MessageId.TraceEnd, line_iter) + end + end +end + +---@private +function TestParser:get_content_until_end_tag(end_tag, line_iter) + local content = {} + + while true do + local line = line_iter() + + if line == nil or vim.startswith(line, end_tag) then + break + end + + table.insert(content, line) + end + + return content +end + +---@private +function TestParser:find_result_node(id) + local function find_node(nodes) + if not nodes or #nodes == 0 then + return + end + + for _, node in ipairs(nodes) do + if node.test_id == id then + return node + end + + local _node = find_node(node.children) + + if _node then + return _node + end + end + end + + return find_node(self.test_details) +end + +return TestParser + +---@class java-test.TestResultExecutionDetails +---@field actual string[] lines +---@field expected string[] lines +---@field status java-test.TestStatus +---@field execution java-test.TestExecutionStatus +---@field trace string[] lines + +---@class java-test.TestResults +---@field display_name string +---@field is_dynamic_test boolean +---@field is_suite boolean +---@field parameter_types string +---@field parent_id integer +---@field test_count integer +---@field test_id integer +---@field test_name string +---@field unique_id string +---@field result java-test.TestResultExecutionDetails +---@field children java-test.TestResults[] diff --git a/lua/java-test/results/result-status.lua b/lua/java-test/results/result-status.lua new file mode 100644 index 0000000..90014bc --- /dev/null +++ b/lua/java-test/results/result-status.lua @@ -0,0 +1,7 @@ +---@enum java-test.TestStatus +local TestStatus = { + Failed = 'failed', + Skipped = 'skipped', +} + +return TestStatus diff --git a/lua/java-test/ui/floating-report-viewer.lua b/lua/java-test/ui/floating-report-viewer.lua new file mode 100644 index 0000000..4cd5401 --- /dev/null +++ b/lua/java-test/ui/floating-report-viewer.lua @@ -0,0 +1,88 @@ +local class = require('java-core.utils.class') +local StringBuilder = require('java-test.utils.string-builder') +local TestStatus = require('java-test.results.result-status') +local ReportViewer = require('java-test.ui.report-viewer') + +---@class java-test.FloatingReportViewer +---@field window number|nil +---@field buffer number|nil +---@overload fun(): java-test.FloatingReportViewer +local FloatingReportViewer = class(ReportViewer) + +---Shows the test results in a floating window +---@param test_results java-test.TestResults[] +function FloatingReportViewer:show(test_results) + ---@param results java-test.TestResults[] + local function build_result(results, indentation, prefix) + local ts = StringBuilder() + + for _, result in ipairs(results) do + local tc = StringBuilder() + + tc.append(prefix .. indentation) + + if result.is_suite then + tc.append(' ' .. result.test_name).lbreak() + else + if result.result.status == TestStatus.Failed then + tc.append('󰅙 ' .. result.test_name).lbreak().append(indentation).append(result.result.trace, indentation) + elseif result.result.status == TestStatus.Skipped then + tc.append(' ' .. result.test_name).lbreak() + else + tc.append(' ' .. result.test_name).lbreak() + end + end + + if result.children then + tc.append(build_result(result.children, indentation .. '\t', '')) + end + + ts.append(tc.build()) + end + + return ts.build() + end + + local res = build_result(test_results, '', '') + + self:show_in_window(vim.split(res, '\n')) +end + +function FloatingReportViewer:show_in_window(content) + local Popup = require('nui.popup') + local event = require('nui.utils.autocmd').event + + local popup = Popup({ + enter = true, + focusable = true, + border = { + style = 'rounded', + }, + position = '50%', + relative = 'editor', + size = { + width = '80%', + height = '60%', + }, + win_options = { + foldmethod = 'indent', + foldlevel = 1, + }, + }) + + -- mount/open the component + popup:mount() + + -- unmount component when cursor leaves buffer + popup:on(event.BufLeave, function() + popup:unmount() + end) + + -- set content + vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, content) + + vim.bo[popup.bufnr].modifiable = false + vim.bo[popup.bufnr].readonly = true +end + +return FloatingReportViewer diff --git a/lua/java-test/ui/report-viewer.lua b/lua/java-test/ui/report-viewer.lua new file mode 100644 index 0000000..2fd853a --- /dev/null +++ b/lua/java-test/ui/report-viewer.lua @@ -0,0 +1,12 @@ +local class = require('java-core.utils.class') + +---@class java-test.ReportViewer +local ReportViewer = class() + +---Shows the test results in a floating window +---@param _ java-test.TestResults[] +function ReportViewer:show(_) + error('not implemented') +end + +return ReportViewer diff --git a/lua/java-test/utils/string-builder.lua b/lua/java-test/utils/string-builder.lua new file mode 100644 index 0000000..91ce178 --- /dev/null +++ b/lua/java-test/utils/string-builder.lua @@ -0,0 +1,30 @@ +local StringBuilder = function() + local str = '' + + local M = {} + M.append = function(text, prefix) + prefix = prefix or '' + if type(text) == 'table' then + for _, line in ipairs(text) do + str = str .. prefix .. line .. '\n' + end + else + assert(type(text) == 'string') + str = str .. prefix .. text + end + return M + end + + M.lbreak = function() + str = str .. '\n' + return M + end + + M.build = function() + return str + end + + return M +end + +return StringBuilder diff --git a/lua/java.lua b/lua/java.lua index 3e3515d..5315e6c 100644 --- a/lua/java.lua +++ b/lua/java.lua @@ -1,98 +1,98 @@ -local decomple_watch = require('java.startup.decompile-watcher') -local mason_dep = require('java.startup.mason-dep') -local setup_wrap = require('java.startup.lspconfig-setup-wrap') -local startup_check = require('java.startup.startup-check') - -local command_util = require('java.utils.command') - -local test_api = require('java.api.test') -local dap_api = require('java.api.dap') local runner_api = require('java.api.runner') local settings_api = require('java.api.settings') local profile_ui = require('java.ui.profile') -local global_config = require('java.config') - local M = {} +---@param custom_config java.PartialConfig | nil function M.setup(custom_config) - vim.api.nvim_exec_autocmds('User', { pattern = 'JavaPreSetup' }) + local default_conf = require('java.config') + local test_api = require('java-test') - local config = - vim.tbl_deep_extend('force', global_config, custom_config or {}) + local config = vim.tbl_deep_extend('force', default_conf, custom_config or {}) vim.g.nvim_java_config = config - vim.api.nvim_exec_autocmds( - 'User', - { pattern = 'JavaSetup', data = { config = config } } - ) - - mason_dep.add_custom_registries(config.mason.registries) - - if not startup_check() then - return + ---------------------------------------------------------------------- + -- neovim version check -- + ---------------------------------------------------------------------- + if config.checks.nvim_version then + if vim.fn.has('nvim-0.11.5') ~= 1 then + local err = require('java-core.utils.errors') + err.throw([[ + nvim-java is only tested on Neovim 0.11.5 or greater + Please upgrade to Neovim 0.11.5 or greater. + If you are sure it works on your version, disable the version check: + checks = { nvim_version = false }' + ]]) + end end - local is_installing = mason_dep.install(config) - - if is_installing then - return + ---------------------------------------------------------------------- + -- logger setup -- + ---------------------------------------------------------------------- + if config.log then + require('java-core.utils.log2').setup(config.log --[[@as java-core.PartialLog2Config]]) end - setup_wrap.setup(config) - decomple_watch.setup() - dap_api.setup_dap_on_lsp_attach() - - vim.api.nvim_exec_autocmds( - 'User', - { pattern = 'JavaPostSetup', data = { config = config } } - ) -end - ----@param path string[] ----@param command fun() ----@param opts vim.api.keyset.user_command -function M.register_api(path, command, opts) - local name = command_util.path_to_command_name(path) - - vim.api.nvim_create_user_command(name, command, opts or {}) + ---------------------------------------------------------------------- + -- package installation -- + ---------------------------------------------------------------------- + local Manager = require('pkgm.manager') + local pkgm = Manager() - local last_index = #path - local func_name = path[last_index] + pkgm:install('jdtls', config.jdtls.version) - table.remove(path, last_index) + if config.java_test.enable then + ---------------------------------------------------------------------- + -- test -- + ---------------------------------------------------------------------- + pkgm:install('java-test', config.java_test.version) - local node = M + M.test = { + run_current_class = test_api.run_current_class, + debug_current_class = test_api.debug_current_class, - for _, v in ipairs(path) do - if not node[v] then - node[v] = {} - end + run_current_method = test_api.run_current_method, + debug_current_method = test_api.debug_current_method, - node = node[v] + view_last_report = test_api.view_last_report, + } end - node[func_name] = command -end + if config.java_debug_adapter.enable then + ---------------------------------------------------------------------- + -- debugger -- + ---------------------------------------------------------------------- + pkgm:install('java-debug', config.java_debug_adapter.version) + require('java-dap').setup() + + M.dap = { + config_dap = function() + require('java-dap').config_dap() + end, + } + end ----------------------------------------------------------------------- --- DAP APIs -- ----------------------------------------------------------------------- -M.dap = {} -M.dap.config_dap = dap_api.config_dap + if config.spring_boot_tools.enable then + pkgm:install('spring-boot-tools', config.spring_boot_tools.version) + end ----------------------------------------------------------------------- --- Test APIs -- ----------------------------------------------------------------------- -M.test = {} -M.test.run_current_class = test_api.run_current_class -M.test.debug_current_class = test_api.debug_current_class + if config.lombok.enable then + pkgm:install('lombok', config.lombok.version) + end -M.test.run_current_method = test_api.run_current_method -M.test.debug_current_method = test_api.debug_current_method + if config.jdk.auto_install then + pkgm:install('openjdk', config.jdk.version) + end -M.test.view_last_report = test_api.view_last_report + ---------------------------------------------------------------------- + -- init -- + ---------------------------------------------------------------------- + require('java.startup.lsp_setup').setup(config) + require('java.startup.decompile-watcher').setup() + require('java-refactor').setup() +end ---------------------------------------------------------------------- -- Runner APIs -- diff --git a/lua/java/api/dap.lua b/lua/java/api/dap.lua deleted file mode 100644 index 8cb0936..0000000 --- a/lua/java/api/dap.lua +++ /dev/null @@ -1,58 +0,0 @@ -local JavaDap = require('java.dap') - -local log = require('java.utils.log') -local get_error_handler = require('java.handlers.error') -local jdtls = require('java.utils.jdtls') -local async = require('java-core.utils.async').sync -local notify = require('java-core.utils.notify') -local project_config = require('java.api.profile_config') - -local M = {} - ----Setup dap config & adapter on jdtls attach event -function M.setup_dap_on_lsp_attach() - log.info('add LspAttach event handlers to setup dap adapter & config') - - M.even_id = vim.api.nvim_create_autocmd('LspAttach', { - callback = M.on_jdtls_attach, - group = vim.api.nvim_create_augroup('nvim-java-dap-config', {}), - }) -end - -function M.config_dap() - log.info('configuring dap') - - return async(function() - local config = vim.g.nvim_java_config - if config.notifications.dap then - notify.warn('Configuring DAP') - end - JavaDap:new(jdtls()):config_dap() - if config.notifications.dap then - notify.info('DAP configured') - end - end) - .catch(get_error_handler('dap configuration failed')) - .run() -end - -function M.on_jdtls_attach(ev) - local client = vim.lsp.get_client_by_id(ev.data.client_id) - - if client == nil then - return - end - - local server_name = client.name - - if server_name == 'jdtls' then - log.info('setup java dap config & adapter') - - project_config.setup() - M.config_dap() - -- removing the event handler after configuring dap - vim.api.nvim_del_autocmd(M.even_id) - end -end - -return M diff --git a/lua/java/api/profile_config.lua b/lua/java/api/profile_config.lua index 3614d02..6389cae 100644 --- a/lua/java/api/profile_config.lua +++ b/lua/java/api/profile_config.lua @@ -1,5 +1,5 @@ -local async = require('java-core.utils.async').sync -local get_error_handler = require('java.handlers.error') +local runner = require('async.runner') +local get_error_handler = require('java-core.utils.error_handler') local class = require('java-core.utils.class') local config_path = vim.fn.stdpath('data') .. '/nvim-java-profiles.json' @@ -89,19 +89,15 @@ function M.load_current_project_profiles() for dap_config_name, dap_config_name_val in pairs(current) do result[dap_config_name] = {} for _, profile in pairs(dap_config_name_val) do - result[dap_config_name][profile.name] = Profile( - profile.vm_args, - profile.prog_args, - profile.name, - profile.is_active - ) + result[dap_config_name][profile.name] = + Profile(profile.vm_args, profile.prog_args, profile.name, profile.is_active) end end return result end function M.save() - return async(function() + return runner(function() local full_config = read_full_config() local updated_profiles = {} for dap_config_name, val in pairs(M.project_profiles) do @@ -157,7 +153,7 @@ end --- @param dap_config_name string --- @param profile_name string function M.set_active_profile(profile_name, dap_config_name) - if not M.__has_profile(profile_name, dap_config_name) then + if not M.has_profile(profile_name, dap_config_name) then return end @@ -175,11 +171,7 @@ end --- @param dap_config_name string --- @param current_profile_name string|nil --- @param new_profile Profile -function M.add_or_update_profile( - new_profile, - current_profile_name, - dap_config_name -) +function M.add_or_update_profile(new_profile, current_profile_name, dap_config_name) assert(new_profile.name, 'Profile name is required') if current_profile_name then M.project_profiles[dap_config_name][current_profile_name] = nil @@ -205,7 +197,7 @@ end --- @param dap_config_name string --- @param profile_name string function M.delete_profile(profile_name, dap_config_name) - if not M.__has_profile(profile_name, dap_config_name) then + if not M.has_profile(profile_name, dap_config_name) then return end @@ -213,10 +205,11 @@ function M.delete_profile(profile_name, dap_config_name) M.save() end +---@private ---Returns true if a profile exists by given name ---@param profile_name string ---@param dap_config_name string -function M.__has_profile(profile_name, dap_config_name) +function M.has_profile(profile_name, dap_config_name) if M.project_profiles[dap_config_name][profile_name] then return true end @@ -228,7 +221,7 @@ end M.Profile = Profile M.setup = function() - async(function() + runner(function() M.current_project_path = vim.fn.getcwd() M.project_profiles = M.load_current_project_profiles() end) diff --git a/lua/java/api/runner.lua b/lua/java/api/runner.lua index d5e34c5..ae1d8aa 100644 --- a/lua/java/api/runner.lua +++ b/lua/java/api/runner.lua @@ -1,6 +1,6 @@ -local async = require('java-core.utils.async').sync -local get_error_handler = require('java.handlers.error') -local Runner = require('java.runner.runner') +local runner = require('async.runner') +local get_error_handler = require('java-core.utils.error_handler') +local Runner = require('java-runner.runner') local M = { built_in = {}, @@ -11,7 +11,7 @@ local M = { --- @param opts {} function M.built_in.run_app(opts) - async(function() + runner(function() M.runner:start_run(opts.args) end) .catch(get_error_handler('Failed to run app')) @@ -19,7 +19,7 @@ function M.built_in.run_app(opts) end function M.built_in.toggle_logs() - async(function() + runner(function() M.runner:toggle_open_log() end) .catch(get_error_handler('Failed to run app')) @@ -27,7 +27,7 @@ function M.built_in.toggle_logs() end function M.built_in.switch_app() - async(function() + runner(function() M.runner:switch_log() end) .catch(get_error_handler('Failed to switch run')) @@ -35,7 +35,7 @@ function M.built_in.switch_app() end function M.built_in.stop_app() - async(function() + runner(function() M.runner:stop_run() end) .catch(get_error_handler('Failed to stop run')) diff --git a/lua/java/api/settings.lua b/lua/java/api/settings.lua index 8cc47a1..0349d90 100644 --- a/lua/java/api/settings.lua +++ b/lua/java/api/settings.lua @@ -1,22 +1,18 @@ -local get_jdtls = require('java.utils.jdtls2') +local lsp_utils = require('java-core.utils.lsp') local JdtlsClient = require('java-core.ls.clients.jdtls-client') local conf_utils = require('java.utils.config') local notify = require('java-core.utils.notify') -local ui = require('java.utils.ui') -local async = require('java-core.utils.async').sync -local get_error_handler = require('java.handlers.error') +local ui = require('java.ui.utils') +local runner = require('async.runner') +local get_error_handler = require('java-core.utils.error_handler') local M = {} function M.change_runtime() - local client = get_jdtls() + local client = lsp_utils.get_jdtls() ---@type RuntimeOption[] - local runtimes = conf_utils.get_property_from_conf( - client.config, - 'settings.java.configuration.runtimes', - {} - ) + local runtimes = conf_utils.get_property_from_conf(client.config, 'settings.java.configuration.runtimes', {}) if #runtimes < 1 then notify.error( @@ -29,18 +25,12 @@ function M.change_runtime() local jdtls = JdtlsClient(client) - async(function() - local sel_runtime = ui.select( - 'Select Runtime', - runtimes, - function(runtime) - return runtime.name .. '::' .. runtime.path - end - ) + runner(function() + local sel_runtime = ui.select('Select Runtime', runtimes, function(runtime) + return runtime.name .. '::' .. runtime.path + end) - for _, runtime in - ipairs(client.config.settings.java.configuration.runtimes) - do + for _, runtime in ipairs(client.config.settings.java.configuration.runtimes) do if sel_runtime.path == runtime.path then runtime.default = true else diff --git a/lua/java/api/test.lua b/lua/java/api/test.lua deleted file mode 100644 index 60aded8..0000000 --- a/lua/java/api/test.lua +++ /dev/null @@ -1,86 +0,0 @@ -local JavaDap = require('java.dap') - -local log = require('java.utils.log') -local jdtls = require('java.utils.jdtls') -local get_error_handler = require('java.handlers.error') - -local async = require('java-core.utils.async').sync - -local JUnitReport = require('java-test.reports.junit') -local ResultParserFactory = require('java-test.results.result-parser-factory') -local ReportViewer = require('java-test.ui.floating-report-viewer') - -local M = { - ---@type java_test.JUnitTestReport - last_report = nil, -} - ----Setup dap config & adapter on jdtls attach event -function M.run_current_class() - log.info('run current test class') - - return async(function() - return JavaDap:new(jdtls()) - :execute_current_test_class(M.get_report(), { noDebug = true }) - end) - .catch(get_error_handler('failed to run the current test class')) - .run() -end - -function M.debug_current_class() - log.info('debug current test class') - - return async(function() - JavaDap:new(jdtls()):execute_current_test_class(M.get_report(), {}) - end) - .catch(get_error_handler('failed to debug the current test class')) - .run() -end - -function M.debug_current_method() - log.info('debug current test method') - - return async(function() - return JavaDap:new(jdtls()) - :execute_current_test_method(M.get_report(), {}) - end) - .catch(get_error_handler('failed to run the current test method')) - .run() -end - -function M.run_current_method() - log.info('run current test method') - - return async(function() - return JavaDap:new(jdtls()) - :execute_current_test_method(M.get_report(), { noDebug = true }) - end) - .catch(get_error_handler('failed to run the current test method')) - .run() -end - -function M.view_last_report() - if M.last_report then - M.last_report:show_report() - end -end - ----@private -function M.config_dap() - log.info('configuring dap') - - return async(function() - JavaDap:new(jdtls()):config_dap() - end) - .catch(get_error_handler('dap configuration failed')) - .run() -end - ----@private -function M.get_report() - local report = JUnitReport(ResultParserFactory(), ReportViewer()) - M.last_report = report - return report -end - -return M diff --git a/lua/java/config.lua b/lua/java/config.lua index 47467ee..b398135 100644 --- a/lua/java/config.lua +++ b/lua/java/config.lua @@ -1,57 +1,74 @@ +local JDTLS_VERSION = '1.43.0' + +local jdtls_version_map = { + ['1.43.0'] = { + lombok = '1.18.40', + java_test = '0.40.1', + java_debug_adapter = '0.58.2', + spring_boot_tools = '1.55.1', + jdk = '17', + }, +} + +local V = jdtls_version_map[JDTLS_VERSION] + ---@class java.Config ----@field root_markers string[] +---@field checks { nvim_version: boolean } ---@field jdtls { version: string } ----@field lombok { version: string } +---@field lombok { enable: boolean, version: string } ---@field java_test { enable: boolean, version: string } ---@field java_debug_adapter { enable: boolean, version: string } ---@field spring_boot_tools { enable: boolean, version: string } ---@field jdk { auto_install: boolean, version: string } ---@field notifications { dap: boolean } ----@field verification { invalid_order: boolean, duplicate_setup_calls: boolean, invalid_mason_registry: boolean } ----@field mason { registries: string[] } +---@field log java-core.Log2Config + +---@class java.PartialConfig +---@field checks? { nvim_version?: boolean } +---@field jdtls? { version?: string } +---@field lombok? { enable?: boolean, version?: string } +---@field java_test? { enable?: boolean, version?: string } +---@field java_debug_adapter? { enable?: boolean, version?: string } +---@field spring_boot_tools? { enable?: boolean, version?: string } +---@field jdk? { auto_install?: boolean, version?: string } +---@field notifications? { dap?: boolean } +---@field log? java-core.PartialLog2Config + +---@type java.Config local config = { - -- list of file that exists in root of the project - root_markers = { - 'settings.gradle', - 'settings.gradle.kts', - 'pom.xml', - 'build.gradle', - 'mvnw', - 'gradlew', - 'build.gradle', - 'build.gradle.kts', - '.git', + checks = { + nvim_version = true, }, jdtls = { - version = 'v1.43.0', + version = JDTLS_VERSION, }, lombok = { - version = 'nightly', + enable = true, + version = V.lombok, }, -- load java test plugins java_test = { enable = true, - version = '0.40.1', + version = V.java_test, }, -- load java debugger plugins java_debug_adapter = { enable = true, - version = '0.58.2', + version = V.java_debug_adapter, }, spring_boot_tools = { enable = true, - version = '1.55.1', + version = V.spring_boot_tools, }, jdk = { - -- install jdk using mason.nvim auto_install = true, - version = '17.0.2', + version = V.jdk, }, notifications = { @@ -59,38 +76,13 @@ local config = { dap = true, }, - -- We do multiple verifications to make sure things are in place to run this - -- plugin - verification = { - -- nvim-java checks for the order of execution of following - -- * require('java').setup() - -- * require('lspconfig').jdtls.setup() - -- IF they are not executed in the correct order, you will see a error - -- notification. - -- Set following to false to disable the notification if you know what you - -- are doing - invalid_order = true, - - -- nvim-java checks if the require('java').setup() is called multiple - -- times. - -- IF there are multiple setup calls are executed, an error will be shown - -- Set following property value to false to disable the notification if - -- you know what you are doing - duplicate_setup_calls = true, - - -- nvim-java checks if nvim-java/mason-registry is added correctly to - -- mason.nvim plugin. - -- IF it's not registered correctly, an error will be thrown and nvim-java - -- will stop setup - invalid_mason_registry = false, - }, - - mason = { - -- These mason registries will be prepended to the existing mason - -- configuration - registries = { - 'github:nvim-java/mason-registry', - }, + log = { + use_console = true, + use_file = true, + level = 'info', + log_file = vim.fn.stdpath('state') .. '/nvim-java.log', + max_lines = 1000, + show_location = false, }, } diff --git a/lua/java/dap/init.lua b/lua/java/dap/init.lua deleted file mode 100644 index 15804a9..0000000 --- a/lua/java/dap/init.lua +++ /dev/null @@ -1,112 +0,0 @@ -local log = require('java.utils.log') -local buf_util = require('java.utils.buffer') -local win_util = require('java.utils.window') - -local async = require('java-core.utils.async').sync -local notify = require('java-core.utils.notify') - -local DapSetup = require('java-dap.api.setup') -local DapRunner = require('java-dap.api.runner') - -local JavaCoreTestApi = require('java-core.api.test') -local profile_config = require('java.api.profile_config') - ----@class JavaDap ----@field private client LspClient ----@field private dap JavaCoreDap ----@field private test_api java_core.TestApi ----@field private test_client java-core.TestClient -local M = {} - ----@param args { client: LspClient } ----@return JavaDap -function M:new(args) - local o = { - client = args.client, - } - - o.test_api = JavaCoreTestApi:new({ - client = args.client, - runner = DapRunner(), - }) - - o.dap = DapSetup(args.client) - - setmetatable(o, self) - self.__index = self - return o -end - ----Run the current test class ----@param report java_test.JUnitTestReport ----@param config JavaCoreDapLauncherConfigOverridable -function M:execute_current_test_class(report, config) - log.debug('running the current class') - - return self.test_api:run_class_by_buffer( - buf_util.get_curr_buf(), - report, - config - ) -end - -function M:execute_current_test_method(report, config) - log.debug('running the current method') - - local method = self:find_current_test_method() - - if not method then - notify.warn('cursor is not on a test method') - return - end - - self.test_api:run_test({ method }, report, config) -end - -function M:find_current_test_method() - log.debug('finding the current test method') - - local cursor = win_util.get_cursor() - local methods = self.test_api:get_test_methods(buf_util.get_curr_uri()) - - for _, method in ipairs(methods) do - local line_start = method.range.start.line - local line_end = method.range['end'].line - - if cursor.line >= line_start and cursor.line <= line_end then - return method - end - end -end - -function M:config_dap() - log.debug('set dap adapter callback function') - local nvim_dap = require('dap') - nvim_dap.adapters.java = function(callback) - async(function() - local adapter = self.dap:get_dap_adapter() - callback(adapter --[[@as Adapter]]) - end).run() - end - local dap_config = self.dap:get_dap_config() - - for _, config in ipairs(dap_config) do - local profile = profile_config.get_active_profile(config.name) - if profile then - config.vmArgs = profile.vm_args - config.args = profile.prog_args - end - end - -- if dap is already running, need to terminate it to apply new config - if nvim_dap.session then - nvim_dap.terminate() - if vim.g.nvim_java_config.notifications.dap then - notify.warn('Terminating current dap session') - end - end - -- end - nvim_dap.configurations.java = nvim_dap.configurations.java or {} - vim.list_extend(nvim_dap.configurations.java, dap_config) -end - -return M diff --git a/lua/java/startup/decompile-watcher.lua b/lua/java/startup/decompile-watcher.lua index 9c3e9c1..d512773 100644 --- a/lua/java/startup/decompile-watcher.lua +++ b/lua/java/startup/decompile-watcher.lua @@ -1,7 +1,7 @@ -local jdtls = require('java.utils.jdtls2') -local get_error_handler = require('java.handlers.error') +local lsp_utils = require('java-core.utils.lsp') +local get_error_handler = require('java-core.utils.error_handler') -local async = require('java-core.utils.async').sync +local runner = require('async.runner') local JavaCoreJdtlsClient = require('java-core.ls.clients.jdtls-client') @@ -21,8 +21,8 @@ function M.setup() callback = function(opts) local done = false - async(function() - local client = jdtls() + runner(function() + local client = lsp_utils.get_jdtls() local buffer = opts.buf local text = JavaCoreJdtlsClient(client):java_decompile(opts.file) @@ -43,7 +43,7 @@ function M.setup() done = true end) - .catch(get_error_handler('decompilation failed for ' .. opts.file)) + .catch(get_error_handler('Decompilation failed for ' .. opts.file)) .run() vim.wait(10000, function() diff --git a/lua/java/startup/duplicate-setup-check.lua b/lua/java/startup/duplicate-setup-check.lua deleted file mode 100644 index cbe38eb..0000000 --- a/lua/java/startup/duplicate-setup-check.lua +++ /dev/null @@ -1,27 +0,0 @@ -local M = {} - -local message = 'require("java").setup() is called more than once' - .. '\nnvim-java will continue to setup but nvim-java configurations might not work as expected' - .. '\nThis might be due to old installation instructions.' - .. '\nPlease check the latest guide at https://github.com/nvim-java/nvim-java#hammer-how-to-install' - .. '\nIf you know what you are doing, you can disable the check from the config' - .. '\nhttps://github.com/nvim-java/nvim-java#wrench-configuration' - -function M.is_valid() - if vim.g.nvim_java_setup_is_called then - return { - success = false, - continue = true, - message = message, - } - end - - vim.g.nvim_java_setup_is_called = true - - return { - success = true, - continue = true, - } -end - -return M diff --git a/lua/java/startup/exec-order-check.lua b/lua/java/startup/exec-order-check.lua deleted file mode 100644 index 259f9f6..0000000 --- a/lua/java/startup/exec-order-check.lua +++ /dev/null @@ -1,48 +0,0 @@ -local lspconfig = require('lspconfig') - -local M = {} - -lspconfig.util.on_setup = lspconfig.util.add_hook_before( - lspconfig.util.on_setup, - function(config) - if config.name == 'jdtls' then - vim.g.nvim_java_jdtls_setup_is_called = true - end - end -) - -local message = 'Looks like require("lspconfig").jdtls.setup() is called before require("java").setup().' - .. '\nnvim-java will continue to setup but most features may not work as expected' - .. '\nThis might be due to old installation instructions.' - .. '\nPlease check the latest guide at https://github.com/nvim-java/nvim-java#hammer-how-to-install' - .. '\nIf you know what you are doing, you can disable the check from the config' - .. '\nhttps://github.com/nvim-java/nvim-java#wrench-configuration' - -function M.is_valid() - if vim.g.nvim_java_jdtls_setup_is_called then - return { - success = false, - continue = true, - message = message, - } - end - - local clients = vim.lsp.get_clients - and vim.lsp.get_clients({ name = 'jdtls' }) - or vim.lsp.get_active_clients({ name = 'jdtls' }) - - if #clients > 0 then - return { - success = false, - continue = true, - message = message, - } - end - - return { - success = true, - continue = true, - } -end - -return M diff --git a/lua/java/startup/lsp_setup.lua b/lua/java/startup/lsp_setup.lua new file mode 100644 index 0000000..ea1646b --- /dev/null +++ b/lua/java/startup/lsp_setup.lua @@ -0,0 +1,43 @@ +local path = require('java-core.utils.path') +local List = require('java-core.utils.list') +local Manager = require('pkgm.manager') + +local server = require('java-core.ls.servers.jdtls') + +local M = {} + +---comment +---@param config java.Config +function M.setup(config) + local jdtls_plugins = List:new() + + if config.java_test.enable then + jdtls_plugins:push('java-test') + end + + if config.java_debug_adapter.enable then + jdtls_plugins:push('java-debug') + end + + if config.spring_boot_tools.enable then + jdtls_plugins:push('spring-boot-tools') + + local spring_boot_root = Manager:get_install_dir('spring-boot-tools', config.spring_boot_tools.version) + + require('spring_boot').setup({ + ls_path = path.join(spring_boot_root, 'extension', 'language-server'), + }) + + require('spring_boot').init_lsp_commands() + end + + local default_config = server.get_config({ + plugins = jdtls_plugins, + use_jdk = config.jdk.auto_install, + use_lombok = config.lombok.enable, + }) + + vim.lsp.config('jdtls', default_config) +end + +return M diff --git a/lua/java/startup/lspconfig-setup-wrap.lua b/lua/java/startup/lspconfig-setup-wrap.lua deleted file mode 100644 index 2c92189..0000000 --- a/lua/java/startup/lspconfig-setup-wrap.lua +++ /dev/null @@ -1,52 +0,0 @@ -local lspconfig = require('lspconfig') -local log = require('java.utils.log') -local mason_util = require('java-core.utils.mason') - -local server = require('java-core.ls.servers.jdtls') - -local M = {} - ----comment ----@param config java.Config -function M.setup(config) - log.info('wrap lspconfig.java.setup function to inject a custom java config') - ---@type fun(config: LspSetupConfig) - local org_setup = lspconfig.jdtls.setup - - lspconfig.jdtls.setup = function(user_config) - vim.api.nvim_exec_autocmds('User', { pattern = 'JavaJdtlsSetup' }) - - local jdtls_plugins = {} - - if config.java_test.enable then - table.insert(jdtls_plugins, 'java-test') - end - - if config.java_debug_adapter.enable then - table.insert(jdtls_plugins, 'java-debug-adapter') - end - - if config.spring_boot_tools.enable then - table.insert(jdtls_plugins, 'spring-boot-tools') - end - - local default_config = server.get_config({ - root_markers = config.root_markers, - jdtls_plugins = jdtls_plugins, - use_mason_jdk = config.jdk.auto_install, - }) - - if config.spring_boot_tools.enable then - require('spring_boot').setup({ - ls_path = mason_util.get_pkg_path('spring-boot-tools') - .. '/extension/language-server', - }) - - require('spring_boot').init_lsp_commands() - end - - org_setup(vim.tbl_extend('force', default_config, user_config)) - end -end - -return M diff --git a/lua/java/startup/mason-dep.lua b/lua/java/startup/mason-dep.lua deleted file mode 100644 index 129c777..0000000 --- a/lua/java/startup/mason-dep.lua +++ /dev/null @@ -1,105 +0,0 @@ -local log = require('java.utils.log') -local mason_ui = require('mason.ui') -local mason_util = require('java.utils.mason') -local list_util = require('java-core.utils.list') -local notify = require('java-core.utils.notify') -local async = require('java-core.utils.async') -local lazy = require('java.ui.lazy') -local sync = async.sync -local mason_v2 = require('mason.version').MAJOR_VERSION == 2 - -local List = require('java-core.utils.list') - -local M = {} - ----Add custom registries to Mason 1.x ----@param registries java.Config -local function add_custom_registries_v1(registries) - local mason_default_config = require('mason.settings').current - - local new_registries = - list_util:new(registries):concat(mason_default_config.registries) - - require('mason').setup({ - registries = new_registries, - }) -end - ----Add custom registries to Mason 2.x ----@param registries java.Config -local function add_custom_registries_v2(registries) - for _, reg in ipairs(registries) do - ---@diagnostic disable-next-line: undefined-field - require('mason-registry').sources:prepend(reg) - end -end - -if mason_v2 then - M.add_custom_registries = add_custom_registries_v2 -else - M.add_custom_registries = add_custom_registries_v1 -end - ----Install mason package dependencies for nvim-java ----@param config java.Config -function M.install(config) - local packages = M.get_pkg_list(config) - local is_outdated = mason_util.is_outdated(packages) - - if is_outdated then - sync(function() - M.refresh_and_install(packages) - end) - .catch(function(err) - notify.error('Failed to setup nvim-java ' .. tostring(err)) - log.error('failed to setup nvim-java ' .. tostring(err)) - end) - .run() - end - - return is_outdated -end - -function M.refresh_and_install(packages) - vim.schedule(function() - -- lazy covers mason - -- https://github.com/nvim-java/nvim-java/issues/51 - lazy.close_lazy_if_opened() - - mason_ui.open() - notify.warn('Please close and re-open after dependencies are installed') - end) - - mason_util.refresh_registry() - mason_util.install_pkgs(packages) -end - ----Returns a list of dependency packages ----@param config java.Config ----@return table -function M.get_pkg_list(config) - local deps = List:new({ - { name = 'jdtls', version = config.jdtls.version }, - { name = 'lombok-nightly', version = config.lombok.version }, - { name = 'java-test', version = config.java_test.version }, - { - name = 'java-debug-adapter', - version = config.java_debug_adapter.version, - }, - }) - - if config.jdk.auto_install then - deps:push({ name = 'openjdk-17', version = config.jdk.version }) - end - - if config.spring_boot_tools.enable then - deps:push({ - name = 'spring-boot-tools', - version = config.spring_boot_tools.version, - }) - end - - return deps -end - -return M diff --git a/lua/java/startup/mason-registry-check.lua b/lua/java/startup/mason-registry-check.lua deleted file mode 100644 index ce8db28..0000000 --- a/lua/java/startup/mason-registry-check.lua +++ /dev/null @@ -1,52 +0,0 @@ -local mason_v2 = require('mason.version').MAJOR_VERSION == 2 - -local mason_sources - -if mason_v2 then - -- compiler will complain when Mason 1.x is used - ---@diagnostic disable-next-line: undefined-field - mason_sources = require('mason-registry').sources -else - mason_sources = require('mason-registry.sources') -end - -local M = {} -if mason_v2 then - M.JAVA_REG_ID = 'nvim-java/mason-registry' -else - M.JAVA_REG_ID = 'github:nvim-java/mason-registry' -end - -function M.is_valid() - local iterator - - if mason_v2 then - -- the compiler will complain when Mason 1.x is in use - ---@diagnostic disable-next-line: undefined-field - iterator = mason_sources.iterate - else - -- the compiler will complain when Mason 2.x is in use - ---@diagnostic disable-next-line: undefined-field - iterator = mason_sources.iter - end - - for reg in iterator(mason_sources) do - if reg.id == M.JAVA_REG_ID then - return { - success = true, - continue = true, - } - end - end - - return { - success = false, - continue = false, - message = 'nvim-java mason registry is not added correctly!' - .. '\nThis occurs when mason.nvim configured incorrectly' - .. '\nPlease refer the link below to fix the issue' - .. '\nhttps://github.com/nvim-java/nvim-java/wiki/Q-&-A#no_entry-cannot-find-package-xxxxx', - } -end - -return M diff --git a/lua/java/startup/nvim-dep.lua b/lua/java/startup/nvim-dep.lua deleted file mode 100644 index 10c485e..0000000 --- a/lua/java/startup/nvim-dep.lua +++ /dev/null @@ -1,67 +0,0 @@ -local log = require('java.utils.log') - -local pkgs = { - { - name = 'mason-registry', - err = [[mason.nvim is not installed. nvim-java requires mason.nvim to install dependecies. - Please follow the install guide in https://github.com/nvim-java/nvim-java to install nvim-java]], - }, - { - name = 'dap', - err = [[nvim-dap is not installed. nvim-java requires nvim-dap to setup the debugger. -Please follow the install guide in https://github.com/nvim-java/nvim-java to install nvim-java]], - }, - { - name = 'lspconfig', - err = [[nvim-lspconfig is not installed. nvim-lspconfig requires nvim-lspconfig to show diagnostics & auto completion. -Please follow the install guide in https://github.com/nvim-java/nvim-java to install nvim-java]], - }, - { - name = 'java-refactor', - warn = [[nvim-java-refactor is not installed. nvim-java-refactor requires nvim-java to do code refactoring -Please add nvim-java-refactor to the current dependency list - -{ - "nvim-java/nvim-java", - dependencies = { - "nvim-java/nvim-java-refactor", - .... - } -} - -Please follow the install guide in https://github.com/nvim-java/nvim-java to install nvim-java]], - }, -} - -local M = {} - -function M.is_valid() - log.info('check neovim plugin dependencies') - - for _, pkg in ipairs(pkgs) do - local ok, _ = pcall(require, pkg.name) - - if not ok then - if pkg.warn then - return { - success = false, - continue = true, - message = pkg.warn, - } - else - return { - success = false, - continue = false, - message = pkg.err, - } - end - end - end - - return { - success = true, - continue = true, - } -end - -return M diff --git a/lua/java/startup/startup-check.lua b/lua/java/startup/startup-check.lua deleted file mode 100644 index a1bf5bd..0000000 --- a/lua/java/startup/startup-check.lua +++ /dev/null @@ -1,53 +0,0 @@ -local log = require('java.utils.log') -local notify = require('java-core.utils.notify') - -local function get_checkers() - local config = vim.g.nvim_java_config - local checks = {} - - if config.verification.invalid_mason_registry then - table.insert( - checks, - select(1, require('java.startup.mason-registry-check')) - ) - end - - if config.verification.invalid_order then - table.insert(checks, select(1, require('java.startup.exec-order-check'))) - end - - if config.verification.duplicate_setup_calls then - table.insert( - checks, - select(1, require('java.startup.duplicate-setup-check')) - ) - end - - table.insert(checks, select(1, require('java.startup.nvim-dep'))) - - return checks -end - -return function() - local checkers = get_checkers() - - for _, check in ipairs(checkers) do - local check_res = check.is_valid() - - if check_res.message then - if not check_res.success then - log.error(check_res.message) - notify.error(check_res.message) - else - log.warn(check_res.message) - notify.warn(check_res.message) - end - end - - if not check_res.continue then - return false - end - end - - return true -end diff --git a/lua/java/treesitter/init.lua b/lua/java/treesitter/init.lua deleted file mode 100644 index 162a5e5..0000000 --- a/lua/java/treesitter/init.lua +++ /dev/null @@ -1,26 +0,0 @@ -local queries = require('java.treesitter.queries') - -local M = {} - ----Finds a main method in the given buffer and returns the line number ----@return integer | nil line number of the main method -function M.find_main_method(buffer) - local query = vim.treesitter.query.parse('java', queries.main_class) - local parser = vim.treesitter.get_parser(buffer, 'java') - local root = parser:parse()[1]:root() - - for _, match, _ in query:iter_matches(root, buffer, 0, -1) do - for id, node in pairs(match) do - local capture_name = query.captures[id] - - if capture_name == 'main_method' then - -- first element is the line number - return ({ node:start() })[1] - end - end - end - - return nil -end - -return M diff --git a/lua/java/treesitter/queries.lua b/lua/java/treesitter/queries.lua deleted file mode 100644 index 78f419a..0000000 --- a/lua/java/treesitter/queries.lua +++ /dev/null @@ -1,15 +0,0 @@ -local M = {} - -M.main_class = [[ -(method_declaration - (modifiers) @modifiers (#eq? @modifiers "public static") - type: (void_type) @return_type - name: (identifier) @name (#eq? @name "main") - parameters: (formal_parameters - (formal_parameter - type: (array_type - element: (type_identifier) @arg_type (#eq? @arg_type "String")))) -) @main_method -]] - -return M diff --git a/lua/java/ui/profile.lua b/lua/java/ui/profile.lua index 54bb1ac..2549c0d 100644 --- a/lua/java/ui/profile.lua +++ b/lua/java/ui/profile.lua @@ -1,15 +1,16 @@ local event = require('nui.utils.autocmd').event -local Layout = require('nui.layout') -local Menu = require('nui.menu') -local Popup = require('nui.popup') local notify = require('java-core.utils.notify') local profile_config = require('java.api.profile_config') local class = require('java-core.utils.class') -local dap_api = require('java.api.dap') -local log = require('java.utils.log') -local DapSetup = require('java-dap.api.setup') -local jdtls = require('java.utils.jdtls') -local ui = require('java.utils.ui') +local dap_api = require('java-dap') +local log = require('java-core.utils.log2') +local lsp_utils = require('java-core.utils.lsp') +local ui = require('java.ui.utils') + +local Layout = require('nui.layout') +local Menu = require('nui.menu') +local Popup = require('nui.popup') +local DapSetup = require('java-dap.setup') local new_profile = 'New Profile' @@ -147,27 +148,22 @@ function ProfileUI:get_menu() }, on_submit = function(item) if item.text == new_profile then - self:_open_profile_editor() + self:open_profile_editor() else local profile_name = clear_active_postfix(item.text) - self:_open_profile_editor(profile_name) + self:open_profile_editor(profile_name) end end, }) end +---@private --- @param title string --- @param key string --- @param target_profile string --- @param enter boolean|nil --- @param keymaps boolean|nil -function ProfileUI:_get_and_fill_popup( - title, - key, - target_profile, - enter, - keymaps -) +function ProfileUI:get_and_fill_popup(title, key, target_profile, enter, keymaps) local style = self.style local text = { top = '[' .. title .. ']', @@ -203,29 +199,12 @@ function ProfileUI:_get_and_fill_popup( return popup end -function ProfileUI:_open_profile_editor(target_profile) +---@private +function ProfileUI:open_profile_editor(target_profile) local popups = { - name = self:_get_and_fill_popup( - 'Name', - 'name', - target_profile, - true, - false - ), - vm_args = self:_get_and_fill_popup( - 'VM arguments', - 'vm_args', - target_profile, - false, - false - ), - prog_args = self:_get_and_fill_popup( - 'Program arguments', - 'prog_args', - target_profile, - false, - true - ), + name = self:get_and_fill_popup('Name', 'name', target_profile, true, false), + vm_args = self:get_and_fill_popup('VM arguments', 'vm_args', target_profile, false, false), + prog_args = self:get_and_fill_popup('Program arguments', 'prog_args', target_profile, false, true), } local layout = Layout( @@ -279,20 +258,18 @@ function ProfileUI:_open_profile_editor(target_profile) ) end +---@private --- @return boolean -function ProfileUI:_is_selected_profile_modifiable() - if - self.focus_item == nil - or self.focus_item.text == nil - or self.focus_item.text == new_profile - then +function ProfileUI:is_selected_profile_modifiable() + if self.focus_item == nil or self.focus_item.text == nil or self.focus_item.text == new_profile then return false end return true end -function ProfileUI:_set_active_profile() - if not self:_is_selected_profile_modifiable() then +---@private +function ProfileUI:set_active_profile() + if not self:is_selected_profile_modifiable() then notify.error('Failed to set profile as active') return end @@ -307,8 +284,9 @@ function ProfileUI:_set_active_profile() self:openMenu() end -function ProfileUI:_delete_profile() - if not self:_is_selected_profile_modifiable() then +---@private +function ProfileUI:delete_profile() + if not self:is_selected_profile_modifiable() then notify.error('Failed to delete profile') return end @@ -339,38 +317,34 @@ function ProfileUI:openMenu() self.menu:unmount() end, { noremap = true, nowait = true }) self.menu:map('n', 'a', function() - self:_set_active_profile() + self:set_active_profile() end, { noremap = true, nowait = true }) -- delete self.menu:map('n', 'd', function() - self:_delete_profile() + self:delete_profile() end, { noremap = true, nowait = true }) end local M = {} -local async = require('java-core.utils.async').sync -local get_error_handler = require('java.handlers.error') +local runner = require('async.runner') +local get_error_handler = require('java-core.utils.error_handler') --- @type ProfileUI M.ProfileUI = ProfileUI function M.ui() - return async(function() - local configs = DapSetup(jdtls().client):get_dap_config() + return runner(function() + local configs = DapSetup(lsp_utils.get_jdtls()):get_dap_config() if not configs or #configs == 0 then notify.error('No classes with main methods are found') return end - local selected_config = ui.select( - 'Select the main class (module -> mainClass)', - configs, - function(config) - return config.name - end - ) + local selected_config = ui.select('Select the main class (module -> mainClass)', configs, function(config) + return config.name + end) if not selected_config then return diff --git a/lua/java/utils/ui.lua b/lua/java/ui/utils.lua similarity index 92% rename from lua/java/utils/ui.lua rename to lua/java/ui/utils.lua index 91860d4..d04a243 100644 --- a/lua/java/utils/ui.lua +++ b/lua/java/ui/utils.lua @@ -73,8 +73,7 @@ function M.multi_select(prompt, values, format_item) prompt = prompt, format_item = function(item) local prefix = item.is_selected and '* ' or '' - return prefix - .. (format_item and format_item(item.value) or item.value) + return prefix .. (format_item and format_item(item.value) or item.value) end, }, function(selected) if not selected then @@ -90,8 +89,7 @@ function M.multi_select(prompt, values, format_item) return end - wrapped_items[selected.index].is_selected = - not wrapped_items[selected.index].is_selected + wrapped_items[selected.index].is_selected = not wrapped_items[selected.index].is_selected open_select() end) diff --git a/lua/java/utils/command.lua b/lua/java/utils/command.lua deleted file mode 100644 index 4170b33..0000000 --- a/lua/java/utils/command.lua +++ /dev/null @@ -1,25 +0,0 @@ -local M = {} - ----Converts a path array to command name ----@param path string[] ----@return string -function M.path_to_command_name(path) - local name = 'Java' - - for _, word in ipairs(path) do - local sub_words = vim.split(word, '_') - local changed_word = '' - - for _, sub_word in ipairs(sub_words) do - local first_char = sub_word:sub(1, 1):upper() - local rest = sub_word:sub(2) - changed_word = changed_word .. first_char .. rest - end - - name = name .. changed_word - end - - return name -end - -return M diff --git a/lua/java/utils/instance_factory.lua b/lua/java/utils/instance_factory.lua deleted file mode 100644 index 56787be..0000000 --- a/lua/java/utils/instance_factory.lua +++ /dev/null @@ -1,24 +0,0 @@ -local class = require('java-core.utils.class') - -local M = class() - ----@return vim.lsp.Client -local function get_client() - local clients = vim.lsp.get_clients({ name = 'jdtls' }) - - if #clients < 1 then - local message = string.format('No jdtls client found to instantiate class') - require('java-core.utils.notify').error(message) - require('java.utils.log').error(message) - error(message) - end - - return clients[1] -end - ----@return java-core.JdtlsClient -function M.jdtls_client() - return require('java-core.ls.clients.jdtls-client')(get_client()) -end - -return M diff --git a/lua/java/utils/jdtls.lua b/lua/java/utils/jdtls.lua deleted file mode 100644 index 80addcd..0000000 --- a/lua/java/utils/jdtls.lua +++ /dev/null @@ -1,17 +0,0 @@ -local get_error_handler = require('java.handlers.error') - ----Returns an active jdtls client ----@return { client: LspClient } -local function get_jdtls() - local clients = vim.lsp.get_active_clients({ name = 'jdtls' }) - - if #clients == 0 then - get_error_handler('could not find an active jdtls client')() - end - - return { - client = clients[1], - } -end - -return get_jdtls diff --git a/lua/java/utils/jdtls2.lua b/lua/java/utils/jdtls2.lua deleted file mode 100644 index 2eb49c9..0000000 --- a/lua/java/utils/jdtls2.lua +++ /dev/null @@ -1,21 +0,0 @@ -local get_error_handler = require('java.handlers.error') - ----Returns an active jdtls client ----@return vim.lsp.Client -local function get_jdtls() - local clients - - if vim.lsp.get_clients then - clients = vim.lsp.get_clients({ name = 'jdtls' }) - else - clients = vim.lsp.get_active_clients({ name = 'jdtls' }) - end - - if #clients == 0 then - get_error_handler('could not find an active jdtls client')() - end - - return clients[1] -end - -return get_jdtls diff --git a/lua/java/utils/mason.lua b/lua/java/utils/mason.lua deleted file mode 100644 index 824d425..0000000 --- a/lua/java/utils/mason.lua +++ /dev/null @@ -1,108 +0,0 @@ -local log = require('java.utils.log') -local mason_reg = require('mason-registry') -local async = require('java-core.utils.async') -local await = async.wait_handle_ok -local mason_v2 = require('mason.version').MAJOR_VERSION == 2 - -local M = {} - -function M.is_available(package_name, package_version) - -- get_package errors if the package is not available in Mason 2.x - -- it works fine in Mason 1.x this way too. - local has_pkg, pkg = pcall(mason_reg.get_package, package_name) - - if not has_pkg then - return false - end - - local installed_version - if mason_v2 then - -- the compiler will complain when Mason 1.x is in use - ---@diagnostic disable-next-line: missing-parameter - installed_version = pkg:get_installed_version() - else - -- the compiler will complain when mason 2.x is in use - ---@diagnostic disable-next-line: param-type-mismatch - pkg:get_installed_version(function(success, version) - if success then - installed_version = version - end - end) - end - - return installed_version == package_version -end - -function M.is_installed(package_name, package_version) - -- get_package errors if the package is not available in Mason 2.x - -- it works fine in Mason 1.x this way too. - local found, pkg = pcall(mason_reg.get_package, package_name) - - if not found or not pkg:is_installed() then - return false - end - - local installed_version - if mason_v2 then - -- the compiler will complain when Mason 1.x is in use - ---@diagnostic disable-next-line: missing-parameter - installed_version = pkg:get_installed_version() - else - -- the compiler will complain when Mason 2.x is in use - ---@diagnostic disable-next-line: param-type-mismatch - pkg:get_installed_version(function(success, version) - if success then - installed_version = version - end - end) - end - - return installed_version == package_version -end - -function M.is_outdated(packages) - for _, pkg in ipairs(packages) do - if not M.is_available(pkg.name, pkg.version) then - return true - end - - if not M.is_installed(pkg.name, pkg.version) then - return true - end - end -end - -function M.refresh_registry() - await(function(callback) - mason_reg.update(callback) - end) -end - -function M.install_pkgs(packages) - log.info('check mason dependecies') - - for _, dep in ipairs(packages) do - if not M.is_installed(dep.name, dep.version) then - local pkg = mason_reg.get_package(dep.name) - - -- install errors if installation is already running in Mason 2.x - local guard - if mason_v2 then - -- guard if the package is already installing in Mason 2.x - -- the compiler will complain about the following line with Mason 1.x - ---@diagnostic disable-next-line: undefined-field - guard = pkg:is_installing() - else - guard = false - end - if not guard then - pkg:install({ - version = dep.version, - force = true, - }) - end - end - end -end - -return M diff --git a/lua/pkgm/downloaders/factory.lua b/lua/pkgm/downloaders/factory.lua new file mode 100644 index 0000000..f041fe6 --- /dev/null +++ b/lua/pkgm/downloaders/factory.lua @@ -0,0 +1,40 @@ +local system = require('java-core.utils.system') +local Wget = require('pkgm.downloaders.wget') +local PowerShell = require('pkgm.downloaders.powershell') +local log = require('java-core.utils.log2') +local err_util = require('java-core.utils.errors') + +local M = {} + +---Get appropriate downloader based on platform and binary availability +---@param opts table Downloader options (url, dest, retry_count, timeout) +---@return table # Downloader instance +function M.get_downloader(opts) + local os = system.get_os() + log.debug('Getting downloader for OS:', os) + + -- On Windows, prefer PowerShell + if os == 'win' then + if vim.fn.executable('pwsh') == 1 or vim.fn.executable('powershell') == 1 then + log.debug('Using PowerShell downloader') + return PowerShell(opts) + end + end + + -- Check for wget on all platforms + if vim.fn.executable('wget') == 1 then + log.debug('Using wget downloader') + return Wget(opts) + end + + -- On Windows, fallback to PowerShell if available + if os == 'win' and (vim.fn.executable('pwsh') == 1 or vim.fn.executable('powershell') == 1) then + log.debug('Using PowerShell downloader (fallback)') + return PowerShell(opts) + end + + local err = 'No downloader available (wget or powershell not found)' + err_util.throw(err) +end + +return M diff --git a/lua/pkgm/downloaders/powershell.lua b/lua/pkgm/downloaders/powershell.lua new file mode 100644 index 0000000..e2236d7 --- /dev/null +++ b/lua/pkgm/downloaders/powershell.lua @@ -0,0 +1,69 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') +local err_util = require('java-core.utils.errors') + +---@class java-core.PowerShell +---@field url string +---@field dest string +---@field retry_count number +---@field timeout number +local PowerShell = class() + +---@class java-core.PowerShellOpts +---@field url string URL to download +---@field dest? string Destination path (optional, uses temp if not provided) +---@field retry_count? number Retry count (optional, defaults to 5) +---@field timeout? number Timeout in seconds (optional, defaults to 30) + +---@param opts java-core.PowerShellOpts +function PowerShell:_init(opts) + self.url = opts.url + + if not opts.dest then + local filename = vim.fs.basename(opts.url) + self.dest = vim.fn.tempname() .. '-' .. filename + log.debug('Using temp destination:', self.dest) + else + self.dest = opts.dest + log.debug('Using provided destination:', self.dest) + end + + self.retry_count = opts.retry_count or 5 + self.timeout = opts.timeout or 30 +end + +---Download file using PowerShell +---@return string # Path to downloaded file +function PowerShell:download() + local pwsh = vim.fn.executable('pwsh') == 1 and 'pwsh' or 'powershell' + log.debug('PowerShell downloading:', self.url, 'to', self.dest) + log.debug('Using PowerShell binary:', pwsh) + + local pwsh_cmd = string.format( + 'iwr -TimeoutSec %d -UseBasicParsing -Method "GET" -Uri %q -OutFile %q;', + self.timeout, + self.url, + self.dest + ) + + local cmd = string.format( + -- luacheck: ignore + "%s -NoProfile -NonInteractive -Command \"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; %s\"", + pwsh, + pwsh_cmd + ) + log.debug('PowerShell command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + local err = string.format('PowerShell download failed (exit %d): %s', exit_code, result) + err_util.throw(err) + end + + log.debug('PowerShell download completed:', self.dest) + return self.dest +end + +return PowerShell diff --git a/lua/pkgm/downloaders/wget.lua b/lua/pkgm/downloaders/wget.lua new file mode 100644 index 0000000..fff6629 --- /dev/null +++ b/lua/pkgm/downloaders/wget.lua @@ -0,0 +1,63 @@ +local path = require('java-core.utils.path') +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-core.Wget +---@field url string +---@field dest string +---@field retry_count number +---@field timeout number +local Wget = class() + +---@class java-core.WgetOpts +---@field url string URL to download +---@field dest? string Destination path (optional, uses temp if not provided) +---@field retry_count? number Retry count (optional, defaults to 5) +---@field timeout? number Timeout in seconds (optional, defaults to 30) + +---@param opts java-core.WgetOpts +function Wget:_init(opts) + self.url = opts.url + + if not opts.dest then + local filename = vim.fs.basename(opts.url) + local tmp_dir = vim.fn.tempname() + vim.fn.mkdir(tmp_dir, 'p') + self.dest = path.join(tmp_dir, filename) + log.debug('Using temp destination:', self.dest) + else + self.dest = opts.dest + log.debug('Using provided destination:', self.dest) + end + + self.retry_count = opts.retry_count or 5 + self.timeout = opts.timeout or 30 +end + +---Download file using wget +---@return string|nil # Path to downloaded file, or nil on failure +---@return string|nil # Error message if failed +function Wget:download() + log.debug('wget downloading:', self.url, 'to', self.dest) + local cmd = string.format( + 'wget -t %d -T %d -O %s %s', + self.retry_count, + self.timeout, + vim.fn.shellescape(self.dest), + vim.fn.shellescape(self.url) + ) + log.debug('wget command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + log.error('wget failed:', exit_code, result) + return nil, string.format('wget failed (exit %d): %s', exit_code, result) + end + + log.debug('wget download completed:', self.dest) + return self.dest, nil +end + +return Wget diff --git a/lua/pkgm/extractors/factory.lua b/lua/pkgm/extractors/factory.lua new file mode 100644 index 0000000..a36b6ba --- /dev/null +++ b/lua/pkgm/extractors/factory.lua @@ -0,0 +1,77 @@ +local system = require('java-core.utils.system') +local Unzip = require('pkgm.extractors.unzip') +local Tar = require('pkgm.extractors.tar') +local PowerShellExtractor = require('pkgm.extractors.powershell') +local Uncompressed = require('pkgm.extractors.uncompressed') +local log = require('java-core.utils.log2') +local err_util = require('java-core.utils.errors') + +local M = {} + +---Get appropriate extractor based on file extension +---@param opts table Extractor options (source, dest) +---@return table # Extractor instance +function M.get_extractor(opts) + local source = opts.source + local lower_source = source:lower() + local os = system.get_os() + log.debug('Getting extractor for:', source, 'on OS:', os) + + -- Check for zip files + if lower_source:match('%.zip$') or lower_source:match('%.vsix$') then + log.debug('Detected zip file') + -- On Windows, prefer PowerShell + if os == 'win' then + if vim.fn.executable('pwsh') == 1 or vim.fn.executable('powershell') == 1 then + log.debug('Using PowerShell extractor') + return PowerShellExtractor(opts) + end + end + + -- Check for unzip on all platforms + if vim.fn.executable('unzip') == 1 then + log.debug('Using unzip extractor') + return Unzip(opts) + end + + -- Fallback to PowerShell on Windows if available + if os == 'win' and (vim.fn.executable('pwsh') == 1 or vim.fn.executable('powershell') == 1) then + log.debug('Using PowerShell extractor (fallback)') + return PowerShellExtractor(opts) + end + + local err = 'No zip extractor available (unzip or powershell not found)' + err_util.throw(err) + end + + -- Check for tar files + if + lower_source:match('%.tar$') + or lower_source:match('%.tar%.gz$') + or lower_source:match('%.tgz$') + or lower_source:match('%.tar%.xz$') + or lower_source:match('%.tar%.bz2$') + then + log.debug('Detected tar file') + local tar_cmd = vim.fn.executable('gtar') == 1 and 'gtar' or 'tar' + if vim.fn.executable(tar_cmd) == 1 then + log.debug('Using tar extractor:', tar_cmd) + return Tar(opts) + else + local err = 'tar not available' + err_util.throw(err) + end + end + + -- Check for jar files + if lower_source:match('%.jar$') then + log.debug('Detected jar file') + log.debug('Using uncompressed extractor') + return Uncompressed(opts) + end + + local err = string.format('Unsupported archive format: %s', source) + err_util.throw(err) +end + +return M diff --git a/lua/pkgm/extractors/powershell.lua b/lua/pkgm/extractors/powershell.lua new file mode 100644 index 0000000..6aec31c --- /dev/null +++ b/lua/pkgm/extractors/powershell.lua @@ -0,0 +1,65 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-core.PowerShellExtractor +---@field source string +---@field dest string +local PowerShellExtractor = class() + +---@class java-core.PowerShellExtractorOpts +---@field source string Path to zip file +---@field dest string Destination directory + +---@param opts java-core.PowerShellExtractorOpts +function PowerShellExtractor:_init(opts) + self.source = opts.source + self.dest = opts.dest +end + +---Extract zip file using PowerShell Expand-Archive +---@return boolean|nil # true on success, nil on failure +---@return string|nil # Error message if failed +function PowerShellExtractor:extract() + local pwsh = vim.fn.executable('pwsh') == 1 and 'pwsh' or 'powershell' + log.debug('PowerShell extracting:', self.source, 'to', self.dest) + log.debug('Using PowerShell binary:', pwsh) + + -- Expand-Archive requires .zip extension + local source_file = self.source + if not source_file:lower():match('%.zip$') then + log.debug('Renaming file to add .zip extension') + source_file = source_file .. '.zip' + local ok = vim.fn.rename(self.source, source_file) + if ok ~= 0 then + log.error('Failed to rename file to .zip extension') + return nil, 'Failed to rename file to .zip extension' + end + end + + local pwsh_cmd = string.format( + 'Microsoft.PowerShell.Archive\\Expand-Archive -Path %q -DestinationPath %q -Force', + source_file, + self.dest + ) + + local cmd = string.format( + --luacheck: ignore + "%s -NoProfile -NonInteractive -Command \"$ProgressPreference = 'SilentlyContinue'; $ErrorActionPreference = 'Stop'; %s\"", + pwsh, + pwsh_cmd + ) + log.debug('PowerShell command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + log.error('PowerShell extraction failed:', exit_code, result) + return nil, string.format('PowerShell extraction failed (exit %d): %s', exit_code, result) + end + + log.debug('PowerShell extraction completed') + return true, nil +end + +return PowerShellExtractor diff --git a/lua/pkgm/extractors/tar.lua b/lua/pkgm/extractors/tar.lua new file mode 100644 index 0000000..58c873b --- /dev/null +++ b/lua/pkgm/extractors/tar.lua @@ -0,0 +1,57 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') +local system = require('java-core.utils.system') + +---@class java-core.Tar +---@field source string +---@field dest string +local Tar = class() + +---@class java-core.TarOpts +---@field source string Path to tar file (supports .tar, .tar.gz, .tgz, .tar.xz) +---@field dest string Destination directory + +---@param opts java-core.TarOpts +function Tar:_init(opts) + self.source = opts.source + self.dest = opts.dest +end + +---Extract tar file using tar +---@return boolean|nil # true on success, nil on failure +---@return string|nil # Error message if failed +function Tar:extract() + local tar_cmd = vim.fn.executable('gtar') == 1 and 'gtar' or 'tar' + log.debug('tar extracting:', self.source, 'to', self.dest) + log.debug('Using tar binary:', tar_cmd) + + local cmd + if system.get_os() == 'win' then + -- Windows: convert backslashes to forward slashes (tar accepts them) + local source = self.source:gsub('\\', '/') + local dest = self.dest:gsub('\\', '/') + cmd = string.format('%s --no-same-owner --force-local -xf "%s" -C "%s"', tar_cmd, source, dest) + else + -- Unix: use shellescape + cmd = string.format( + '%s --no-same-owner -xf %s -C %s', + tar_cmd, + vim.fn.shellescape(self.source), + vim.fn.shellescape(self.dest) + ) + end + log.debug('tar command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + log.error('tar extraction failed:', exit_code, result) + return nil, string.format('tar failed (exit %d): %s', exit_code, result) + end + + log.debug('tar extraction completed') + return true, nil +end + +return Tar diff --git a/lua/pkgm/extractors/uncompressed.lua b/lua/pkgm/extractors/uncompressed.lua new file mode 100644 index 0000000..da2fe1b --- /dev/null +++ b/lua/pkgm/extractors/uncompressed.lua @@ -0,0 +1,47 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-core.Uncompressed +---@field source string +---@field dest string +local Uncompressed = class() + +---@class java-core.UncompressedOpts +---@field source string Path to jar file +---@field dest string Destination directory + +---@param opts java-core.UncompressedOpts +function Uncompressed:_init(opts) + self.source = opts.source + self.dest = opts.dest +end + +---Move jar file to destination +---@return boolean|nil # true on success, nil on failure +---@return string|nil # Error message if failed +function Uncompressed:extract() + log.debug('Moving uncompressed file:', self.source, 'to', self.dest) + + if not self.source:lower():match('%.jar$') then + local err = 'Only .jar files are supported' + log.error(err) + return nil, err + end + + local filename = vim.fn.fnamemodify(self.source, ':t') + local dest_path = vim.fn.resolve(self.dest .. '/' .. filename) + + log.debug('Destination path:', dest_path) + + local success = vim.loop.fs_copyfile(self.source, dest_path) + if not success then + local err = string.format('Failed to copy %s to %s', self.source, dest_path) + log.error(err) + return nil, err + end + + log.debug('File move completed') + return true, nil +end + +return Uncompressed diff --git a/lua/pkgm/extractors/unzip.lua b/lua/pkgm/extractors/unzip.lua new file mode 100644 index 0000000..5314703 --- /dev/null +++ b/lua/pkgm/extractors/unzip.lua @@ -0,0 +1,39 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') + +---@class java-core.Unzip +---@field source string +---@field dest string +local Unzip = class() + +---@class java-core.UnzipOpts +---@field source string Path to zip file +---@field dest string Destination directory + +---@param opts java-core.UnzipOpts +function Unzip:_init(opts) + self.source = opts.source + self.dest = opts.dest +end + +---Extract zip file using unzip +---@return boolean|nil # true on success, nil on failure +---@return string|nil # Error message if failed +function Unzip:extract() + log.debug('unzip extracting:', self.source, 'to', self.dest) + local cmd = string.format('unzip -q -o %s -d %s', vim.fn.shellescape(self.source), vim.fn.shellescape(self.dest)) + log.debug('unzip command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + log.error('unzip extraction failed:', exit_code, result) + return nil, string.format('unzip failed (exit %d): %s', exit_code, result) + end + + log.debug('unzip extraction completed') + return true, nil +end + +return Unzip diff --git a/lua/pkgm/manager.lua b/lua/pkgm/manager.lua new file mode 100644 index 0000000..f5b8257 --- /dev/null +++ b/lua/pkgm/manager.lua @@ -0,0 +1,183 @@ +local class = require('java-core.utils.class') +local path = require('java-core.utils.path') +local downloader_factory = require('pkgm.downloaders.factory') +local extractor_factory = require('pkgm.extractors.factory') +local log = require('java-core.utils.log2') +local default_specs = require('pkgm.specs.init') +local notify = require('java-core.utils.notify') +local err_util = require('java-core.utils.errors') + +---@class pkgm.Manager +---@field specs pkgm.PackageSpec[] +local Manager = class() + +Manager.packages_root = path.join(vim.fn.stdpath('data'), 'nvim-java', 'packages') + +---@param specs? pkgm.PackageSpec[] +function Manager:_init(specs) + self.specs = specs or default_specs + log.debug('Manager initialized with ' .. #self.specs .. ' specs') +end + +---Download and extract a package +---@param name string +---@param version string +---@return string # Installation directory path +function Manager:install(name, version) + log.debug('Installing package:', name, version) + + if self:is_installed(name, version) then + local install_dir = self:get_install_dir(name, version) + log.debug('Package already installed:', install_dir) + return install_dir + end + + notify.info('Installing package ' .. name .. ' version ' .. version) + + local spec = self:find_spec(name, version) + local url = spec:get_url(name, version) + local downloaded_file = self:download_package(url) + local install_dir = self:get_install_dir(name, version) + + log.debug('Install directory:', install_dir) + + self:extract_package(downloaded_file, install_dir) + + log.debug('Package installed successfully:', install_dir) + + return install_dir +end + +---Check if package is installed +---@param name string +---@param version string +---@return boolean # true if package is installed +function Manager:is_installed(name, version) + local install_dir = self:get_install_dir(name, version) + local installed = vim.fn.isdirectory(install_dir) == 1 + log.debug('Checking if package installed:', name, version, installed) + return installed +end + +---Uninstall a package +---@param name string +---@param version string +---@return boolean|nil # true on success, nil on failure +---@return string|nil # Error message if failed +function Manager:uninstall(name, version) + log.debug('Uninstalling package:', name, version) + local install_dir = self:get_install_dir(name, version) + + if vim.fn.isdirectory(install_dir) == 0 then + log.warn('Package not installed:', install_dir) + return nil, 'Package not installed' + end + + log.debug('Deleting directory:', install_dir) + local result = vim.fn.delete(install_dir, 'rf') + if result ~= 0 then + log.error('Failed to delete package directory:', install_dir) + return nil, 'Failed to delete package directory' + end + + log.debug('Package uninstalled successfully') + return true, nil +end + +---Find matching spec for package name and version +---@private +---@param name string +---@param version string +---@return pkgm.PackageSpec # Matching spec, or nil if not found +function Manager:find_spec(name, version) + log.debug('Finding spec for ' .. name .. ' version ' .. version) + + for _, spec in ipairs(self.specs) do + if spec:is_match(name, version) then + log.debug('Found matching spec') + return spec + end + end + + local err = string.format('No matching spec for %s version %s', name, version) + err_util.throw(err) +end + +---Get platform-specific URL from package spec +---@private +---@param spec pkgm.PackageSpec +---@param name string +---@param version string +---@return string|nil # URL for current platform, or nil if not available +---@return string|nil # Error message if URL not available +function Manager:get_platform_url(spec, name, version) + local success, result = pcall(function() + return spec:get_url(name, version) + end) + + if not success then + return nil, result + end + + return result, nil +end + +---Get package installation directory +---@param name string +---@param version string +---@return string # Installation directory path +function Manager:get_install_dir(name, version) + return path.join(Manager.packages_root, name, version) +end + +---Download package from URL +---@private +---@param url string +---@return string # Downloaded file path +function Manager:download_package(url) + log.debug('Using URL:', url) + + local downloader = downloader_factory.get_downloader({ url = url }) + + log.debug('Starting download...') + + local downloaded_file, err = downloader:download() + + if not downloaded_file then + err_util.throw(err or 'Download failed') + end + + log.debug('Downloaded to:', downloaded_file) + + return downloaded_file +end + +---Extract package to installation directory +---@private +---@param downloaded_file string +---@param install_dir string +function Manager:extract_package(downloaded_file, install_dir) + local extractor = extractor_factory.get_extractor({ + source = downloaded_file, + dest = install_dir, + }) + + vim.fn.mkdir(install_dir, 'p') + + log.debug('Starting extraction...') + + local success, err = extractor:extract() + + if not success then + vim.fn.delete(install_dir, 'rf') + vim.fn.delete(downloaded_file) + err_util.throw(err or 'Extraction failed') + end + + log.debug('Extraction completed') + + vim.fn.delete(downloaded_file) + log.debug('Cleaned up temporary file') +end + +return Manager diff --git a/lua/pkgm/pkgs/jdtls.lua b/lua/pkgm/pkgs/jdtls.lua new file mode 100644 index 0000000..e69de29 diff --git a/lua/pkgm/specs/base-spec.lua b/lua/pkgm/specs/base-spec.lua new file mode 100644 index 0000000..817d12d --- /dev/null +++ b/lua/pkgm/specs/base-spec.lua @@ -0,0 +1,220 @@ +local class = require('java-core.utils.class') +local log = require('java-core.utils.log2') +local system = require('java-core.utils.system') +local err = require('java-core.utils.errors') + +---@class pkgm.VersionRange +---@field from string +---@field to string + +---@alias pkgm.UrlValue string|table + +---@class pkgm.BaseSpecConfig +---@field name string +---@field version string|'*' +---@field version_range? pkgm.VersionRange +---@field urls? table +---@field url? string|pkgm.UrlValue +---@field [string] string + +---@class pkgm.BaseSpec: pkgm.PackageSpec +local BaseSpec = class() + +---@param config pkgm.BaseSpecConfig +function BaseSpec:_init(config) + log.debug('Initializing BaseSpec with config: ' .. vim.inspect(config)) + + self._name = config.name + self._version = config.version + self._version_range = config.version_range + self._template_vars = {} + + if not config.url and not config.urls then + err.throw('BaseSpec: Neither url nor urls provided') + end + + self._url = config.url + self._urls = config.urls + + for key, value in pairs(config) do + if key ~= 'name' and key ~= 'version' and key ~= 'version_range' and key ~= 'urls' and key ~= 'url' then + self._template_vars[key] = value + end + end + + log.debug('Template vars: ' .. vim.inspect(self._template_vars)) +end + +---@return string +function BaseSpec:get_name() + log.debug('get_name() returning: ' .. self._name) + return self._name +end + +---@return string +function BaseSpec:get_version() + log.debug('get_version() returning: ' .. self._version) + return self._version +end + +---@param name string +---@param version string +---@return boolean +function BaseSpec:is_match(name, version) + local version_desc = self._version or 'range' + log.debug( + 'is_match() checking name=' + .. name + .. ' version=' + .. version + .. ' against spec name=' + .. self._name + .. ' spec version=' + .. version_desc + ) + + if name ~= self._name then + log.debug('Name mismatch') + return false + end + + if self._version == '*' then + log.debug('Wildcard version match') + return true + end + + if self._version_range then + local in_range = self:is_version_in_range(version, self._version_range.from, self._version_range.to) + log.debug('Version range check: ' .. tostring(in_range)) + return in_range + end + + local exact_match = version == self._version + log.debug('Exact version match: ' .. tostring(exact_match)) + return exact_match +end + +---@param name string +---@param version string +---@return string +function BaseSpec:get_url(name, version) + log.debug('get_url() called with name=' .. name .. ' version=' .. version) + + if self._url then + log.debug('Resolving url') + local url_template = self:resolve_hierarchical_url(self._url) + return self:parse_url_template(url_template, name, version) + end + + if not self._urls then + err.throw('BaseSpec: Neither url nor urls provided') + end + + log.debug('Resolving urls table') + local url_template = self:resolve_hierarchical_url(self._urls) + + if not url_template then + err.throw('BaseSpec: No url found for current system configuration') + end + + return self:parse_url_template(url_template, name, version) +end + +---@private +---@param url_value string|table +---@return string|nil +function BaseSpec:resolve_hierarchical_url(url_value) + if type(url_value) == 'string' then + log.debug('URL is string: ' .. url_value) + return url_value + end + + if type(url_value) ~= 'table' then + log.error('URL value must be string or table') + return nil + end + + local platform = system.get_os() + log.debug('Platform: ' .. platform) + + local platform_value = url_value[platform] + if not platform_value then + log.error('No URL for platform: ' .. platform) + return nil + end + + if type(platform_value) == 'string' then + log.debug('Platform-specific URL: ' .. platform_value) + return platform_value + end + + local arch = system.get_arch() + log.debug('Architecture: ' .. arch) + + local arch_value = platform_value[arch] + if not arch_value then + log.error('No URL for architecture: ' .. arch) + return nil + end + + if type(arch_value) == 'string' then + log.debug('Architecture-specific URL: ' .. arch_value) + return arch_value + end + + local bit_depth = system.get_bit_depth() + log.debug('Bit depth: ' .. bit_depth) + + local bit_value = arch_value[bit_depth] + if not bit_value or type(bit_value) ~= 'string' then + log.error('No URL for bit depth: ' .. bit_depth) + return nil + end + + log.debug('Bit-depth-specific URL: ' .. bit_value) + return bit_value +end + +---@private +---@param url_template string +---@param name string +---@param version string +---@return string +function BaseSpec:parse_url_template(url_template, name, version) + local vars = vim.tbl_extend('force', { + name = name, + version = version, + }, self._template_vars) + + return self:parse_template(url_template, vars) +end + +---@private +---@param version string +---@param from string +---@param to string +---@return boolean +function BaseSpec:is_version_in_range(version, from, to) + log.debug('Checking if ' .. version .. ' is between ' .. from .. ' and ' .. to) + return version >= from and version <= to +end + +---@protected +---@param template string +---@param vars table +---@return string +function BaseSpec:parse_template(template, vars) + log.debug('Parsing template: ' .. template) + local result = template + + for key, value in pairs(vars) do + local pattern = '{{' .. key .. '}}' + result = result:gsub(pattern, value) + log.debug('Replaced ' .. pattern .. ' with ' .. value) + end + + log.debug('Parsed result: ' .. result) + return result +end + +return BaseSpec diff --git a/lua/pkgm/specs/init.lua b/lua/pkgm/specs/init.lua new file mode 100644 index 0000000..2396fb1 --- /dev/null +++ b/lua/pkgm/specs/init.lua @@ -0,0 +1,78 @@ +local BaseSpec = require('pkgm.specs.base-spec') +local JdtlsSpec = require('pkgm.specs.jdtls-spec') + +---@class pkgm.PackageSpec +---@field get_name fun(self: pkgm.PackageSpec): string +---@field get_version fun(self: pkgm.PackageSpec): string +---@field get_url fun(self: pkgm.PackageSpec, name: string, version: string): string +---@field is_match fun(self: pkgm.PackageSpec, name: string, version: string): boolean + +return { + JdtlsSpec({ + name = 'jdtls', + version_range = { from = '1.43.0', to = '1.53.0' }, + url = 'https://download.eclipse.org/{{name}}/milestones/' + .. '{{version}}/jdt-language-server-{{version}}-{{timestamp}}.tar.gz', + }), + BaseSpec({ + name = 'java-test', + version = '*', + url = 'https://openvsxorg.blob.core.windows.net/resources/vscjava/vscode-java-test' + .. '/{{version}}/vscjava.vscode-java-test-{{version}}.vsix', + }), + + BaseSpec({ + name = 'java-debug', + version = '*', + url = 'https://openvsxorg.blob.core.windows.net/resources/vscjava/vscode-java-debug/' + .. '{{version}}/vscjava.vscode-java-debug-{{version}}.vsix', + }), + + BaseSpec({ + name = 'spring-boot-tools', + version = '*', + url = 'https://openvsxorg.blob.core.windows.net/resources/VMware/vscode-spring-boot' + .. '/{{version}}/VMware.vscode-spring-boot-{{version}}.vsix', + }), + + BaseSpec({ + name = 'lombok', + version = 'nightly', + url = 'https://projectlombok.org/lombok-edge.jar', + }), + + BaseSpec({ + name = 'lombok', + version = '*', + url = 'https://projectlombok.org/downloads/lombok-{{version}}.jar', + }), + + BaseSpec({ + name = 'openjdk', + version = '17', + full_version = '17.0.12', + urls = { + linux = { + arm = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/archive/jdk-{{full_version}}_linux-aarch64_bin.tar.gz', + }, + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/archive/jdk-{{full_version}}_linux-x64_bin.tar.gz', + }, + }, + mac = { + arm = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/archive/jdk-{{full_version}}_macos-aarch64_bin.tar.gz', + }, + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/archive/jdk-{{full_version}}_macos-x64_bin.tar.gz', + }, + }, + win = { + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/archive/jdk-{{full_version}}_windows-x64_bin.zip', + }, + }, + }, + }), +} diff --git a/lua/pkgm/specs/jdtls-spec/init.lua b/lua/pkgm/specs/jdtls-spec/init.lua new file mode 100644 index 0000000..4622a0b --- /dev/null +++ b/lua/pkgm/specs/jdtls-spec/init.lua @@ -0,0 +1,42 @@ +local class = require('java-core.utils.class') +local BaseSpec = require('pkgm.specs.base-spec') +local version_map = require('pkgm.specs.jdtls-spec.version-map') +local err = require('java-core.utils.errors') + +---@class pkgm.JdtlsSpec: pkgm.BaseSpec +local JdtlsSpec = class(BaseSpec) + +function JdtlsSpec:_init(config) + ---@diagnostic disable-next-line: undefined-field + self:super(config) +end + +function JdtlsSpec:get_url(name, version) + ---@diagnostic disable-next-line: undefined-field + local url = self._base.get_url(self, name, version) + + if not version_map[version] then + local message = string.format( + [[ + %s@%s is not defined in the version map. + You can update the version map yourself and create a PR. + nvim-java/lua/pkgm/specs/jdtls-spec/version-map.lua + or + Please create an issue at: + https://github.com/s1n7ax/nvim-java/issues to add the missing version. + ]], + name, + version + ) + + err.throw(message) + end + + local new_url = self:parse_template(url, { + timestamp = version_map[version], + }) + + return new_url +end + +return JdtlsSpec diff --git a/lua/pkgm/specs/jdtls-spec/version-map.lua b/lua/pkgm/specs/jdtls-spec/version-map.lua new file mode 100644 index 0000000..62282f9 --- /dev/null +++ b/lua/pkgm/specs/jdtls-spec/version-map.lua @@ -0,0 +1,22 @@ +-- To update JDTLS version map: +-- 1. Visit https://download.eclipse.org/jdtls/milestones/ +-- 2. Click on version link in 'Directory Contents' section +-- 3. Find file like: jdt-language-server-X.Y.Z-YYYYMMDDHHSS.tar.gz +-- 4. Extract package version (X.Y.Z) and timestamp (YYYYMMDDHHSS) +-- 5. Add entry: ['X.Y.Z'] = 'YYYYMMDDHHSS' +-- Example: jdt-language-server-1.43.0-202412191447.tar.gz +-- → ['1.43.0'] = '202412191447' +return { + ['1.43.0'] = '202412191447', + ['1.44.0'] = '202501221502', + ['1.45.0'] = '202502271238', + ['1.46.0'] = '202503271314', + ['1.46.1'] = '202504011455', + ['1.47.0'] = '202505151856', + ['1.48.0'] = '202506271502', + ['1.49.0'] = '202507311558', + ['1.50.0'] = '202509041425', + ['1.51.0'] = '202510022025', + ['1.52.0'] = '202510301627', + ['1.53.0'] = '202511192211', +} diff --git a/plugin/java.lua b/plugin/java.lua index a2d4a83..524dc3d 100644 --- a/plugin/java.lua +++ b/plugin/java.lua @@ -7,15 +7,39 @@ end local cmd_map = { JavaSettingsChangeRuntime = { java.settings.change_runtime }, - JavaDapConfig = { java.dap.config_dap }, - - JavaTestRunCurrentClass = { java.test.run_current_class }, - JavaTestDebugCurrentClass = { java.test.debug_current_class }, - - JavaTestRunCurrentMethod = { java.test.run_current_method }, - JavaTestDebugCurrentMethod = { java.test.debug_current_method }, - - JavaTestViewLastReport = { java.test.view_last_report }, + JavaDapConfig = { + function() + require('java-dap').config_dap() + end, + }, + + JavaTestRunCurrentClass = { + function() + require('java-test').run_current_class() + end, + }, + JavaTestDebugCurrentClass = { + function() + require('java-test').debug_current_class() + end, + }, + + JavaTestRunCurrentMethod = { + function() + require('java-test').run_current_method() + end, + }, + JavaTestDebugCurrentMethod = { + function() + require('java-test').debug_current_method() + end, + }, + + JavaTestViewLastReport = { + function() + require('java-test').view_last_report() + end, + }, JavaRunnerRunMain = { java.runner.built_in.run_app, { nargs = '?' } }, JavaRunnerStopMain = { java.runner.built_in.stop_app }, diff --git a/tests/constants/capabilities.lua b/tests/constants/capabilities.lua new file mode 100644 index 0000000..a7a9d89 --- /dev/null +++ b/tests/constants/capabilities.lua @@ -0,0 +1,79 @@ +local List = require('java-core.utils.list') + +local M = {} + +M.required_cmds = List:new({ + 'java.completion.onDidSelect', + 'java.decompile', + 'java.edit.handlePasteEvent', + 'java.edit.organizeImports', + 'java.edit.smartSemicolonDetection', + 'java.edit.stringFormatting', + 'java.navigate.openTypeHierarchy', + 'java.navigate.resolveTypeHierarchy', + 'java.project.addToSourcePath', + 'java.project.changeImportedProjects', + 'java.project.createModuleInfo', + 'java.project.getAll', + 'java.project.getClasspaths', + 'java.project.getSettings', + 'java.project.import', + 'java.project.isTestFile', + 'java.project.listSourcePaths', + 'java.project.refreshDiagnostics', + 'java.project.removeFromSourcePath', + 'java.project.resolveSourceAttachment', + 'java.project.resolveStackTraceLocation', + 'java.project.resolveText', + 'java.project.resolveWorkspaceSymbol', + 'java.project.updateClassPaths', + 'java.project.updateJdk', + 'java.project.updateSettings', + 'java.project.updateSourceAttachment', + 'java.project.upgradeGradle', + 'java.protobuf.generateSources', + 'java.reloadBundles', + 'java.vm.getAllInstalls', + 'sts.java.addClasspathListener', + 'sts.java.code.completions', + 'sts.java.hierarchy.subtypes', + 'sts.java.hierarchy.supertypes', + 'sts.java.javadoc', + 'sts.java.javadocHoverLink', + 'sts.java.location', + 'sts.java.removeClasspathListener', + 'sts.java.search.packages', + 'sts.java.search.types', + 'sts.java.type', + 'sts.project.gav', + 'vscode.java.buildWorkspace', + 'vscode.java.checkProjectSettings', + 'vscode.java.fetchPlatformSettings', + 'vscode.java.fetchUsageData', + 'vscode.java.inferLaunchCommandLength', + 'vscode.java.isOnClasspath', + 'vscode.java.resolveBuildFiles', + 'vscode.java.resolveClassFilters', + 'vscode.java.resolveClasspath', + 'vscode.java.resolveElementAtSelection', + 'vscode.java.resolveInlineVariables', + 'vscode.java.resolveJavaExecutable', + 'vscode.java.resolveMainClass', + 'vscode.java.resolveMainMethod', + 'vscode.java.resolveSourceUri', + 'vscode.java.startDebugSession', + 'vscode.java.test.findDirectTestChildrenForClass', + 'vscode.java.test.findJavaProjects', + 'vscode.java.test.findTestLocation', + 'vscode.java.test.findTestPackagesAndTypes', + 'vscode.java.test.findTestTypesAndMethods', + 'vscode.java.test.generateTests', + 'vscode.java.test.get.testpath', + 'vscode.java.test.junit.argument', + 'vscode.java.test.navigateToTestOrTarget', + 'vscode.java.test.resolvePath', + 'vscode.java.updateDebugSettings', + 'vscode.java.validateLaunchConfig', +}) + +return M diff --git a/tests/prepare-config.lua b/tests/prepare-config.lua deleted file mode 100644 index cf2d89e..0000000 --- a/tests/prepare-config.lua +++ /dev/null @@ -1,66 +0,0 @@ -local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' - ----@diagnostic disable: assign-type-mismatch ----@param path string ----@return string|nil -local function local_plug(path) - return vim.fn.isdirectory(path) == 1 and path or nil -end - -if not vim.loop.fs_stat(lazypath) then - vim.fn.system({ - 'git', - 'clone', - '--filter=blob:none', - 'https://github.com/folke/lazy.nvim.git', - '--branch=stable', - lazypath, - }) -end - -vim.opt.rtp:prepend(lazypath) - -local temp_path = './.test_plugins' - -require('lazy').setup({ - { - 'nvim-lua/plenary.nvim', - lazy = false, - }, - { - 'nvim-java/nvim-java-test', - ---@diagnostic disable-next-line: assign-type-mismatch - dir = local_plug('~/Workspace/nvim-java-test'), - lazy = false, - }, - { - 'nvim-java/nvim-java-core', - ---@diagnostic disable-next-line: assign-type-mismatch - dir = local_plug('~/Workspace/nvim-java-core'), - lazy = false, - }, - { - 'nvim-java/nvim-java-dap', - ---@diagnostic disable-next-line: assign-type-mismatch - dir = local_plug('~/Workspace/nvim-java-dap'), - lazy = false, - }, - { - 'neovim/nvim-lspconfig', - lazy = false, - }, - { - 'mason-org/mason.nvim', - lazy = false, - }, - { - 'MunifTanjim/nui.nvim', - lazy = false, - }, -}, { - root = temp_path, - lockfile = temp_path .. '/lazy-lock.json', - defaults = { - lazy = false, - }, -}) diff --git a/tests/specs/capabilities_spec.lua b/tests/specs/capabilities_spec.lua new file mode 100644 index 0000000..1fe36f1 --- /dev/null +++ b/tests/specs/capabilities_spec.lua @@ -0,0 +1,28 @@ +local lsp_utils = dofile('tests/utils/lsp-utils.lua') +local capabilities = dofile('tests/constants/capabilities.lua') +local List = require('java-core.utils.list') +local assert = require('luassert') +local log = require('java-core.utils.log2') + +describe('LSP Capabilities', function() + it('should have all required commands', function() + vim.cmd.edit('HelloWorld.java') + + local client = lsp_utils.wait_for_lsp_attach('jdtls', 30000) + local commands = client.server_capabilities.executeCommandProvider.commands + local actual_cmds = List:new(commands) + + for _, required_cmd in ipairs(capabilities.required_cmds) do + assert.is_true(actual_cmds:contains(required_cmd), 'Missing required command: ' .. required_cmd) + end + + local extra_cmds = actual_cmds:filter(function(cmd) + return not capabilities.required_cmds:contains(cmd) + end) + + if #extra_cmds > 0 then + log.error('Additional commands found that are not in required list:', extra_cmds) + error('Additional commands found that are not in required list:' .. vim.inspect(extra_cmds)) + end + end) +end) diff --git a/tests/specs/jdtls_extensions_spec.lua b/tests/specs/jdtls_extensions_spec.lua new file mode 100644 index 0000000..6d8f41d --- /dev/null +++ b/tests/specs/jdtls_extensions_spec.lua @@ -0,0 +1,34 @@ +local lsp_utils = dofile('tests/utils/lsp-utils.lua') +local assert = require('luassert') + +describe('JDTLS Extensions', function() + it('should bundle java-test, java-debug, and spring-boot-tools extensions', function() + vim.cmd.edit('HelloWorld.java') + + local client = lsp_utils.wait_for_lsp_attach('jdtls', 30000) + local bundles = client.config.init_options.bundles + + assert.is_not_nil(bundles, 'Bundles should be configured') + assert.is_true(#bundles > 0, 'Bundles should not be empty') + + local has_java_test = false + local has_java_debug = false + local has_spring_boot = false + + for _, bundle in ipairs(bundles) do + if bundle:match('java%-test') and bundle:match('com%.microsoft%.java%.test%.plugin') then + has_java_test = true + end + if bundle:match('java%-debug') and bundle:match('com%.microsoft%.java%.debug%.plugin') then + has_java_debug = true + end + if bundle:match('spring%-boot%-tools') and bundle:match('jdt%-ls%-extension%.jar') then + has_spring_boot = true + end + end + + assert.is_true(has_java_test, 'java-test extension (com.microsoft.java.test.plugin) should be bundled') + assert.is_true(has_java_debug, 'java-debug extension (com.microsoft.java.debug.plugin) should be bundled') + assert.is_true(has_spring_boot, 'spring-boot-tools extension (jdt-ls-extension.jar) should be bundled') + end) +end) diff --git a/tests/specs/lsp_spec.lua b/tests/specs/lsp_spec.lua new file mode 100644 index 0000000..8ef81b7 --- /dev/null +++ b/tests/specs/lsp_spec.lua @@ -0,0 +1,14 @@ +local lsp_utils = dofile('tests/utils/lsp-utils.lua') +local assert = require('luassert') + +describe('LSP Attach', function() + it('should attach when opening a Java buffer', function() + vim.cmd.edit('HelloWorld.java') + + local jdtls = lsp_utils.wait_for_lsp_attach('jdtls', 30000) + local spring = lsp_utils.wait_for_lsp_attach('spring-boot', 30000) + + assert.is_not_nil(jdtls, 'JDTLS should attach to Java buffer') + assert.is_not_nil(spring, 'Spring Boot should attach to Java buffer') + end) +end) diff --git a/tests/test-config.lua b/tests/test-config.lua deleted file mode 100644 index bf89a3f..0000000 --- a/tests/test-config.lua +++ /dev/null @@ -1,26 +0,0 @@ ----@diagnostic disable: assign-type-mismatch ----@param dev_path string ----@param plug_path string ----@return string|nil -local function local_plug(dev_path, plug_path) - return (vim.fn.isdirectory(dev_path) == 1) and dev_path or plug_path -end - -local plug_path = './.test_plugins' - -vim.opt.rtp:append(plug_path .. '/plenary.nvim') -vim.opt.rtp:append(plug_path .. '/nvim-lspconfig') -vim.opt.rtp:append(plug_path .. '/mason.nvim') -vim.opt.rtp:append(plug_path .. '/nui.nvim') - -vim.opt.rtp:append( - local_plug('~/Workspace/nvim-java-core', plug_path .. '/nvim-java-core') -) - -vim.opt.rtp:append( - local_plug('~/Workspace/nvim-java-test', plug_path .. '/nvim-java-test') -) - -vim.opt.rtp:append( - local_plug('~/Workspace/nvim-java-dap', plug_path .. '/nvim-java-dap') -) diff --git a/tests/utils/lsp-utils.lua b/tests/utils/lsp-utils.lua new file mode 100644 index 0000000..c5ea757 --- /dev/null +++ b/tests/utils/lsp-utils.lua @@ -0,0 +1,25 @@ +local M = {} + +---Wait for LSP client to attach +---@param name string LSP client name +---@param timeout? number Timeout in milliseconds (defaults to 30000) +---@return vim.lsp.Client client The attached LSP client +function M.wait_for_lsp_attach(name, timeout) + timeout = timeout or 30000 + + local is_attached = function() + local clients = vim.lsp.get_clients({ name = name }) + return #clients > 0 + end + + local success = vim.wait(timeout, is_attached, 100) + + if not success then + error(string.format('LSP client "%s" failed to attach within %dms', name, timeout)) + end + + local clients = vim.lsp.get_clients({ name = name }) + return clients[1] +end + +return M diff --git a/tests/utils/prepare-config.lua b/tests/utils/prepare-config.lua new file mode 100644 index 0000000..544f21e --- /dev/null +++ b/tests/utils/prepare-config.lua @@ -0,0 +1,56 @@ +local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' + +if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + 'git', + 'clone', + '--filter=blob:none', + 'https://github.com/folke/lazy.nvim.git', + '--branch=stable', + lazypath, + }) +end + +vim.opt.rtp:prepend(lazypath) + +local temp_path = './.test_plugins' + +-- Setup lazy.nvim +require('lazy').setup({ + { + 'nvim-lua/plenary.nvim', + lazy = false, + }, + 'MunifTanjim/nui.nvim', + 'mfussenegger/nvim-dap', + { + 'JavaHello/spring-boot.nvim', + commit = '218c0c26c14d99feca778e4d13f5ec3e8b1b60f0', + }, + { + 'nvim-java/nvim-java', + dir = '.', + config = function() + require('java').setup({ + jdk = { + auto_install = false, + }, + }) + vim.lsp.enable('jdtls') + end, + }, +}, { + root = temp_path, + lockfile = temp_path .. '/lazy-lock.json', + defaults = { lazy = false }, +}) + +vim.api.nvim_create_autocmd('LspAttach', { + callback = function(args) + -- stylua: ignore + vim.lsp.completion.enable(true, args.data.client_id, args.buf, { autotrigger = true }) + vim.keymap.set('i', '', function() + vim.lsp.completion.get() + end, { buffer = args.buf }) + end, +}) diff --git a/tests/utils/test-config.lua b/tests/utils/test-config.lua new file mode 100644 index 0000000..bd1fb1f --- /dev/null +++ b/tests/utils/test-config.lua @@ -0,0 +1,28 @@ +local jdtls_cache_path = vim.fn.stdpath('cache') .. '/jdtls' +local gradle_cache_path = vim.fn.expand('~') .. '/.gradle' + +print('removing cache') +vim.fn.delete(jdtls_cache_path, 'rf') +vim.fn.delete(gradle_cache_path, 'rf') + +vim.o.swapfile = false +vim.o.backup = false +vim.o.writebackup = false + +local temp_path = './.test_plugins' + +vim.opt.runtimepath:append(temp_path .. '/') +vim.opt.runtimepath:append(temp_path .. '/nui.nvim') +vim.opt.runtimepath:append(temp_path .. '/spring-boot.nvim') +vim.opt.runtimepath:append(temp_path .. '/nvim-dap') +vim.opt.runtimepath:append('.') + +local is_nixos = vim.fn.filereadable('/etc/NIXOS') == 1 + +require('java').setup({ + jdk = { + auto_install = not is_nixos, + }, +}) + +vim.lsp.enable('jdtls') From c2044eb0412adfbdb3b25f94aac32a4b01403813 Mon Sep 17 00:00:00 2001 From: Grace Petryk Date: Fri, 12 Sep 2025 18:49:35 -0700 Subject: [PATCH 02/27] status colors + handling for skipped/errored test results --- lua/java-test/reports/junit.lua | 4 ++-- lua/java-test/results/message-id.lua | 2 -- lua/java-test/results/result-parser.lua | 21 ++++++++++++++++++++- lua/java-test/results/result-status.lua | 11 ++++++++--- lua/java-test/ui/floating-report-viewer.lua | 21 ++++++++++++++------- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/lua/java-test/reports/junit.lua b/lua/java-test/reports/junit.lua index 01edb1e..4ad687e 100644 --- a/lua/java-test/reports/junit.lua +++ b/lua/java-test/reports/junit.lua @@ -2,7 +2,7 @@ local class = require('java-core.utils.class') local log = require('java-core.utils.log2') ---@class java-test.JUnitTestReport ----@field private conn uv_tcp_t +---@field private conn uv.uv_tcp_t ---@field private result_parser java-test.TestParser ---@field private result_parser_fac java-test.TestParserFactory ---@field private report_viewer java-test.ReportViewer @@ -29,7 +29,7 @@ function JUnitReport:show_report() end ---Returns a stream reader function ----@param conn uv_tcp_t +---@param conn uv.uv_tcp_t ---@return fun(err: string, buffer: string) # callback function function JUnitReport:get_stream_reader(conn) self.conn = conn diff --git a/lua/java-test/results/message-id.lua b/lua/java-test/results/message-id.lua index 28a37a5..5e11174 100644 --- a/lua/java-test/results/message-id.lua +++ b/lua/java-test/results/message-id.lua @@ -23,8 +23,6 @@ local MessageId = { ActualEnd = '%ACTUALE', TraceStart = '%TRACES', TraceEnd = '%TRACEE', - IGNORE_TEST_PREFIX = '@Ignore: ', - ASSUMPTION_FAILED_TEST_PREFIX = '@AssumptionFailure: ', } return MessageId diff --git a/lua/java-test/results/result-parser.lua b/lua/java-test/results/result-parser.lua index 00468fc..583333a 100644 --- a/lua/java-test/results/result-parser.lua +++ b/lua/java-test/results/result-parser.lua @@ -19,6 +19,13 @@ TestParser.node_parsers = { [MessageId.TestStart] = 'parse_test_start', [MessageId.TestEnd] = 'parse_test_end', [MessageId.TestFailed] = 'parse_test_failed', + [MessageId.TestError] = 'parse_test_failed', +} + +---@private +TestParser.skip_prefixes = { + '@Ignore:', + '@AssumptionFailure:', } ---@private @@ -101,6 +108,12 @@ function TestParser:parse_test_end(data) local node = self:find_result_node(test_id) assert(node) node.result.execution = TestExecStatus.Ended + + for _, prefix in ipairs(TestParser.skip_prefixes) do + if string.match(data[2], '^'..prefix) then + node.result.status = TestStatus.Skipped + end + end end ---@private @@ -109,7 +122,13 @@ function TestParser:parse_test_failed(data, line_iter) local node = self:find_result_node(test_id) assert(node) - node.result.status = TestStatus.Failed + node.result.status = node.result.status or TestStatus.Failed + + for _, prefix in ipairs(TestParser.skip_prefixes) do + if string.match(data[2], '^'..prefix) then + node.result.status = TestStatus.Skipped + end + end while true do local line = line_iter() diff --git a/lua/java-test/results/result-status.lua b/lua/java-test/results/result-status.lua index 90014bc..697632b 100644 --- a/lua/java-test/results/result-status.lua +++ b/lua/java-test/results/result-status.lua @@ -1,7 +1,12 @@ ----@enum java-test.TestStatus +---@class java-test.TestStatus +---@field icon string +---@field highlight string + +---@type { [string]: java-test.TestStatus} local TestStatus = { - Failed = 'failed', - Skipped = 'skipped', + Failed = { icon = ' ', highlight = 'DiagnosticError'}, + Skipped = { icon = ' ', highlight = 'DiagnosticWarn'}, + Passed = { icon = ' ', highlight = 'DiagnosticOk'}, } return TestStatus diff --git a/lua/java-test/ui/floating-report-viewer.lua b/lua/java-test/ui/floating-report-viewer.lua index 4cd5401..85ebb80 100644 --- a/lua/java-test/ui/floating-report-viewer.lua +++ b/lua/java-test/ui/floating-report-viewer.lua @@ -24,12 +24,10 @@ function FloatingReportViewer:show(test_results) if result.is_suite then tc.append(' ' .. result.test_name).lbreak() else + local status = result.result.status or TestStatus.Passed + tc.append(status.icon .. result.test_name).lbreak() if result.result.status == TestStatus.Failed then - tc.append('󰅙 ' .. result.test_name).lbreak().append(indentation).append(result.result.trace, indentation) - elseif result.result.status == TestStatus.Skipped then - tc.append(' ' .. result.test_name).lbreak() - else - tc.append(' ' .. result.test_name).lbreak() + tc.append(indentation).append(result.result.trace, indentation) end end @@ -45,10 +43,19 @@ function FloatingReportViewer:show(test_results) local res = build_result(test_results, '', '') - self:show_in_window(vim.split(res, '\n')) + FloatingReportViewer.show_in_window(vim.split(res, '\n')) end -function FloatingReportViewer:show_in_window(content) +function FloatingReportViewer.show_in_window(content) + vim.api.nvim_create_autocmd('BufWinEnter', { + once = true, + callback = function() + for _, status in pairs(TestStatus) do + vim.fn.matchadd(status.highlight, status.icon) + end + end, + }) + local Popup = require('nui.popup') local event = require('nui.utils.autocmd').event From 1466e03ff01dc8627f74acdebfbab1181c0bcf7f Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 15:39:56 +0530 Subject: [PATCH 03/27] chore: code format --- lua/java-test/results/result-parser.lua | 4 ++-- lua/java-test/results/result-status.lua | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/java-test/results/result-parser.lua b/lua/java-test/results/result-parser.lua index 583333a..3a0d1e2 100644 --- a/lua/java-test/results/result-parser.lua +++ b/lua/java-test/results/result-parser.lua @@ -110,7 +110,7 @@ function TestParser:parse_test_end(data) node.result.execution = TestExecStatus.Ended for _, prefix in ipairs(TestParser.skip_prefixes) do - if string.match(data[2], '^'..prefix) then + if string.match(data[2], '^' .. prefix) then node.result.status = TestStatus.Skipped end end @@ -125,7 +125,7 @@ function TestParser:parse_test_failed(data, line_iter) node.result.status = node.result.status or TestStatus.Failed for _, prefix in ipairs(TestParser.skip_prefixes) do - if string.match(data[2], '^'..prefix) then + if string.match(data[2], '^' .. prefix) then node.result.status = TestStatus.Skipped end end diff --git a/lua/java-test/results/result-status.lua b/lua/java-test/results/result-status.lua index 697632b..cff718e 100644 --- a/lua/java-test/results/result-status.lua +++ b/lua/java-test/results/result-status.lua @@ -1,12 +1,12 @@ ----@class java-test.TestStatus +---@class java-test.TestStatus ---@field icon string ---@field highlight string ---@type { [string]: java-test.TestStatus} local TestStatus = { - Failed = { icon = ' ', highlight = 'DiagnosticError'}, - Skipped = { icon = ' ', highlight = 'DiagnosticWarn'}, - Passed = { icon = ' ', highlight = 'DiagnosticOk'}, + Failed = { icon = ' ', highlight = 'DiagnosticError' }, + Skipped = { icon = ' ', highlight = 'DiagnosticWarn' }, + Passed = { icon = ' ', highlight = 'DiagnosticOk' }, } return TestStatus From 59090527ebb79ea91f938356446cc5bb30323071 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 17:00:24 +0530 Subject: [PATCH 04/27] fix: workspace_execute calling client command handler --- lua/java-core/ls/clients/jdtls-client.lua | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lua/java-core/ls/clients/jdtls-client.lua b/lua/java-core/ls/clients/jdtls-client.lua index 6a33eac..98ecc01 100644 --- a/lua/java-core/ls/clients/jdtls-client.lua +++ b/lua/java-core/ls/clients/jdtls-client.lua @@ -94,15 +94,11 @@ end ---@param arguments? lsp.LSPAny[] ---@param buffer? integer ---@return lsp.LSPAny -function JdtlsClient:workspace_execute_command(command, arguments, buffer) - return await(function(callback) - self.client:exec_cmd( - ---@diagnostic disable-next-line: missing-fields - { command = command, arguments = arguments }, - { bufnr = buffer }, - callback - ) - end) +function JdtlsClient:workspace_execute_command(command, params, buffer) + return self:request('workspace/executeCommand', { + command = command, + arguments = params, + }, buffer) end ---@class jdtls.ResourceMoveDestination From bd122ebcabae67ecd5cc618cfe7ac807730dc495 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 17:00:34 +0530 Subject: [PATCH 05/27] chore: add keymap in dev config --- .devcontainer/config/nvim/init.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/config/nvim/init.lua b/.devcontainer/config/nvim/init.lua index 56e8214..2b7a52d 100644 --- a/.devcontainer/config/nvim/init.lua +++ b/.devcontainer/config/nvim/init.lua @@ -123,4 +123,8 @@ vim.keymap.set('n', 'dt', function() require('dap').terminate() end, { desc = 'Terminate' }) +vim.keymap.set('n', 'gd', function() + vim.lsp.buf.definition() +end, { desc = 'Terminate' }) + vim.keymap.set('n', 'm', "vnewput = execute('messages')") From c7de47037cf932e835d1fecb9d76b1c2caa4bde5 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 17:33:09 +0530 Subject: [PATCH 06/27] chore: add debug messages --- lua/java-test/api.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lua/java-test/api.lua b/lua/java-test/api.lua index e4879fd..f222a7f 100644 --- a/lua/java-test/api.lua +++ b/lua/java-test/api.lua @@ -37,6 +37,8 @@ end ---@param file_uri string uri of the class ---@return java-core.TestDetailsWithRange[] # list of test methods function M:get_test_methods(file_uri) + log.debug('finding test methods for uri: ' .. file_uri) + local classes = self.test_client:find_test_types_and_methods(file_uri) local methods = {} @@ -48,6 +50,8 @@ function M:get_test_methods(file_uri) end end + log.debug('found ' .. #methods .. ' test methods') + return methods end @@ -56,6 +60,8 @@ end ---@param report java-test.JUnitTestReport ---@param config? java-dap.DapLauncherConfigOverridable config to override the default values in test launcher config function M:run_class_by_buffer(buffer, report, config) + log.debug('running test class from buffer: ' .. buffer) + local tests = self:get_test_class_by_buffer(buffer) if #tests < 1 then @@ -63,6 +69,8 @@ function M:run_class_by_buffer(buffer, report, config) return end + log.debug('found ' .. #tests .. ' test classes') + self:run_test(tests, report, config) end @@ -82,10 +90,16 @@ end ---@param report java-test.JUnitTestReport ---@param config? java-dap.DapLauncherConfigOverridable config to override the default values in test launcher config function M:run_test(tests, report, config) + log.debug('running ' .. #tests .. ' tests') + local launch_args = self.test_client:resolve_junit_launch_arguments(test_adapters.tests_to_junit_launch_params(tests)) + log.debug('resolved launch args - mainClass: ' .. launch_args.mainClass .. ', projectName: ' .. launch_args.projectName) + local java_exec = self.debug_client:resolve_java_executable(launch_args.mainClass, launch_args.projectName) + log.debug('java executable: ' .. vim.inspect(java_exec)) + local dap_launcher_config = dap_adapters.junit_launch_args_to_dap_config(launch_args, java_exec, { debug = true, label = 'Launch All Java Tests', @@ -93,6 +107,8 @@ function M:run_test(tests, report, config) dap_launcher_config = vim.tbl_deep_extend('force', dap_launcher_config, config or {}) + log.debug('launching tests with config: ' .. vim.inspect(dap_launcher_config)) + self.runner:run_by_config(dap_launcher_config, report) end From 4a21dfe75eda44522504544265cf040ded5be84c Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 18:44:47 +0530 Subject: [PATCH 07/27] refactor: move checks to separate directory - Create lua/java/checks/ directory structure - Extract nvim version check to nvim-version.lua - Add nvim-jdtls conflict check - Update config to support nvim_jdtls_conflict check --- lua/java.lua | 14 ++------------ lua/java/checks/init.lua | 10 ++++++++++ lua/java/checks/nvim-jdtls.lua | 21 +++++++++++++++++++++ lua/java/checks/nvim-version.lua | 21 +++++++++++++++++++++ lua/java/config.lua | 5 +++-- 5 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 lua/java/checks/init.lua create mode 100644 lua/java/checks/nvim-jdtls.lua create mode 100644 lua/java/checks/nvim-version.lua diff --git a/lua/java.lua b/lua/java.lua index 5315e6c..59332f3 100644 --- a/lua/java.lua +++ b/lua/java.lua @@ -14,19 +14,9 @@ function M.setup(custom_config) vim.g.nvim_java_config = config ---------------------------------------------------------------------- - -- neovim version check -- + -- checks -- ---------------------------------------------------------------------- - if config.checks.nvim_version then - if vim.fn.has('nvim-0.11.5') ~= 1 then - local err = require('java-core.utils.errors') - err.throw([[ - nvim-java is only tested on Neovim 0.11.5 or greater - Please upgrade to Neovim 0.11.5 or greater. - If you are sure it works on your version, disable the version check: - checks = { nvim_version = false }' - ]]) - end - end + require('java.checks').run(config) ---------------------------------------------------------------------- -- logger setup -- diff --git a/lua/java/checks/init.lua b/lua/java/checks/init.lua new file mode 100644 index 0000000..15e29f3 --- /dev/null +++ b/lua/java/checks/init.lua @@ -0,0 +1,10 @@ +local M = {} + +---Run all checks +---@param config java.Config +function M.run(config) + require('java.checks.nvim-version'):run(config) + require('java.checks.nvim-jdtls'):run(config) +end + +return M diff --git a/lua/java/checks/nvim-jdtls.lua b/lua/java/checks/nvim-jdtls.lua new file mode 100644 index 0000000..b3e6290 --- /dev/null +++ b/lua/java/checks/nvim-jdtls.lua @@ -0,0 +1,21 @@ +local M = {} + +---Check if nvim-jdtls plugin is installed +---@param config java.Config +function M:run(config) + if not config.checks.nvim_jdtls_conflict then + return + end + + local ok = pcall(require, 'jdtls') + if ok then + local err = require('java-core.utils.errors') + err.throw([[ + nvim-jdtls plugin detected! + nvim-java and nvim-jdtls should not be used together. + Please remove nvim-jdtls from your configuration. + ]]) + end +end + +return M diff --git a/lua/java/checks/nvim-version.lua b/lua/java/checks/nvim-version.lua new file mode 100644 index 0000000..300c932 --- /dev/null +++ b/lua/java/checks/nvim-version.lua @@ -0,0 +1,21 @@ +local M = {} + +---Run nvim version check +---@param config java.Config +function M:run(config) + if not config.checks.nvim_version then + return + end + + if vim.fn.has('nvim-0.11.5') ~= 1 then + local err = require('java-core.utils.errors') + err.throw([[ + nvim-java is only tested on Neovim 0.11.5 or greater + Please upgrade to Neovim 0.11.5 or greater. + If you are sure it works on your version, disable the version check: + checks = { nvim_version = false }' + ]]) + end +end + +return M diff --git a/lua/java/config.lua b/lua/java/config.lua index b398135..8c9439d 100644 --- a/lua/java/config.lua +++ b/lua/java/config.lua @@ -13,7 +13,7 @@ local jdtls_version_map = { local V = jdtls_version_map[JDTLS_VERSION] ---@class java.Config ----@field checks { nvim_version: boolean } +---@field checks { nvim_version: boolean, nvim_jdtls_conflict: boolean } ---@field jdtls { version: string } ---@field lombok { enable: boolean, version: string } ---@field java_test { enable: boolean, version: string } @@ -24,7 +24,7 @@ local V = jdtls_version_map[JDTLS_VERSION] ---@field log java-core.Log2Config ---@class java.PartialConfig ----@field checks? { nvim_version?: boolean } +---@field checks? { nvim_version?: boolean, nvim_jdtls_conflict?: boolean } ---@field jdtls? { version?: string } ---@field lombok? { enable?: boolean, version?: string } ---@field java_test? { enable?: boolean, version?: string } @@ -38,6 +38,7 @@ local V = jdtls_version_map[JDTLS_VERSION] local config = { checks = { nvim_version = true, + nvim_jdtls_conflict = true, }, jdtls = { From 452e440c84c15f3b3aeae82a5d9c956b72773331 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 18:45:57 +0530 Subject: [PATCH 08/27] feat: add jproperties filetype support - Extract filetype config to separate module - Add jproperties to supported filetypes --- lua/java-core/ls/servers/jdtls/filetype.lua | 10 ++++++++++ lua/java-core/ls/servers/jdtls/init.lua | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 lua/java-core/ls/servers/jdtls/filetype.lua diff --git a/lua/java-core/ls/servers/jdtls/filetype.lua b/lua/java-core/ls/servers/jdtls/filetype.lua new file mode 100644 index 0000000..3fe9ecb --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/filetype.lua @@ -0,0 +1,10 @@ +local M = {} + +function M.get_filetypes() + return { + 'java', + 'jproperties', + } +end + +return M diff --git a/lua/java-core/ls/servers/jdtls/init.lua b/lua/java-core/ls/servers/jdtls/init.lua index 8367a36..271625b 100644 --- a/lua/java-core/ls/servers/jdtls/init.lua +++ b/lua/java-core/ls/servers/jdtls/init.lua @@ -8,6 +8,7 @@ function M.get_config(opts) local cmd = require('java-core.ls.servers.jdtls.cmd') local env = require('java-core.ls.servers.jdtls.env') local root = require('java-core.ls.servers.jdtls.root') + local filetype = require('java-core.ls.servers.jdtls.filetype') local log = require('java-core.utils.log2') log.debug('get_config called with opts:', opts) @@ -17,8 +18,8 @@ function M.get_config(opts) base_conf.cmd = cmd.get_cmd(opts) base_conf.cmd_env = env.get_env(opts) base_conf.init_options.bundles = plugins.get_plugins(opts) - base_conf.filetypes = { 'java' } base_conf.root_markers = root.get_root_markers() + base_conf.filetypes = filetype.get_filetypes() return base_conf end From eff3614ffe6defa7f455a96914e3869e4b2a9c48 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 18:46:16 +0530 Subject: [PATCH 09/27] chore: remove nvim-jdtls from devcontainer --- .devcontainer/config/nvim/lazy-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.devcontainer/config/nvim/lazy-lock.json b/.devcontainer/config/nvim/lazy-lock.json index c076196..73cbd4c 100644 --- a/.devcontainer/config/nvim/lazy-lock.json +++ b/.devcontainer/config/nvim/lazy-lock.json @@ -2,5 +2,6 @@ "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, "nvim-dap": { "branch": "master", "commit": "b38f7d30366d9169d0a623c4c85fbcf99d8d58bb" }, + "nvim-jdtls": { "branch": "master", "commit": "943e2398aba6b7e976603708450c6c93c600e830" }, "spring-boot.nvim": { "branch": "main", "commit": "218c0c26c14d99feca778e4d13f5ec3e8b1b60f0" } } From 1c17cf2975367602f63a3683e1fabc5ef3227e56 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 18:46:27 +0530 Subject: [PATCH 10/27] docs: update README for v4.0.0 release --- README.md | 498 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 497 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16d1022..c540b14 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,500 @@ ![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white) ![Lua](https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white) -monorepo is a complete rewrite. Do not use this plugin. This will be released as nvim-java 4.0.0 in the original repo soon. +Just install and start writing `public static void main(String[] args)`. + +> [!CAUTION] +> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` before installing this + +> [!TIP] +> You can find cool tips & tricks here https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks + +> [!NOTE] +> If you are facing errors while using, please check troubleshoot wiki https://github.com/nvim-java/nvim-java/wiki/Troubleshooting + +## :loudspeaker: Demo + + + +## :dizzy: Features + +- :white_check_mark: Spring Boot Tools +- :white_check_mark: Diagnostics & Auto Completion +- :white_check_mark: Automatic Debug Configuration +- :white_check_mark: Organize Imports & Code Formatting +- :white_check_mark: Running Tests +- :white_check_mark: Run & Debug Profiles +- :white_check_mark: [Code Actions](https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks#running-code-actions) + +## :bulb: Why + +- Everything necessary will be installed automatically +- Uses [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) to setup `jdtls` +- Realtime server settings updates is possible using [neoconf](https://github.com/folke/neoconf.nvim) +- Auto loads necessary `jdtls` plugins + - Supported plugins are, + - `spring-boot-tools` + - `lombok` + - `java-test` + - `java-debug-adapter` + +## :hammer: How to Install + +
+ +:small_orange_diamond:details + +### Starter Configs (Recommend for newbies) + +Following are forks of original repositories pre-configured for java. If you +don't know how to get started, use one of the following to get started. +You can click on **n commits ahead of** link to see the changes made on top of the original project + +- [LazyVim](https://github.com/nvim-java/starter-lazyvim) +- [Kickstart](https://github.com/nvim-java/starter-kickstart) +- [AstroNvim](https://github.com/nvim-java/starter-astronvim) + +### Custom Configuration Instructions + +- Install the plugin + +Using [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +return {'nvim-java/nvim-java'} +``` + +- Setup nvim-java before `lspconfig` + +```lua +require('java').setup() +``` + +- Setup jdtls like you would usually do + +```lua +require('lspconfig').jdtls.setup({}) +``` + +Yep! That's all :) + +
+ +## :keyboard: Commands + +
+ +:small_orange_diamond:details + +### Build + +- `JavaBuildBuildWorkspace` - Runs a full workspace build + +- `JavaBuildCleanWorkspace` - Clear the workspace cache + (for now you have to close and reopen to restart the language server after + the deletion) + +### Runner + +- `JavaRunnerRunMain` - Runs the application or selected main class (if there + are multiple main classes) + +```vim +:JavaRunnerRunMain +:JavaRunnerRunMain +``` + +- `JavaRunnerStopMain` - Stops the running application +- `JavaRunnerToggleLogs` - Toggle between show & hide runner log window + +### DAP + +- `JavaDapConfig` - DAP is autoconfigured on start up, but in case you want to + force configure it again, you can use this API + +### Test + +- `JavaTestRunCurrentClass` - Run the test class in the active buffer +- `JavaTestDebugCurrentClass` - Debug the test class in the active buffer +- `JavaTestRunCurrentMethod` - Run the test method on the cursor +- `JavaTestDebugCurrentMethod` - Debug the test method on the cursor +- `JavaTestViewLastReport` - Open the last test report in a popup window + +### Profiles + +- `JavaProfile` - Opens the profiles UI + +### Refactor + +- `JavaRefactorExtractVariable` - Create a variable from value at cursor/selection +- `JavaRefactorExtractVariableAllOccurrence` - Create a variable for all + occurrences from value at cursor/selection +- `JavaRefactorExtractConstant` - Create a constant from the value at cursor/selection +- `JavaRefactorExtractMethod` - Create a method from the value at cursor/selection +- `JavaRefactorExtractField` - Create a field from the value at cursor/selection + +### Settings + +- `JavaSettingsChangeRuntime` - Change the JDK version to another + +
+ +## :computer: APIs + +
+ +:small_orange_diamond:details + +### Build + +- `build.build_workspace` - Runs a full workspace build + +```lua +require('java').build.build_workspace() +``` + +- `build.clean_workspace` - Clear the workspace cache + (for now you have to close and reopen to restart the language server after + the deletion) + +```lua +require('java').build.clean_workspace() +``` + +### Runner + +- `built_in.run_app` - Runs the application or selected main class (if there + are multiple main classes) + +```lua +require('java').runner.built_in.run_app({}) +require('java').runner.built_in.run_app({'arguments', 'to', 'pass', 'to', 'main'}) +``` + +- `built_in.stop_app` - Stops the running application + +```lua +require('java').runner.built_in.stop_app() +``` + +- `built_in.toggle_logs` - Toggle between show & hide runner log window + +```lua +require('java').runner.built_in.toggle_logs() +``` + +### DAP + +- `config_dap` - DAP is autoconfigured on start up, but in case you want to force + configure it again, you can use this API + +```lua +require('java').dap.config_dap() +``` + +### Test + +- `run_current_class` - Run the test class in the active buffer + +```lua +require('java').test.run_current_class() +``` + +- `debug_current_class` - Debug the test class in the active buffer + +```lua +require('java').test.debug_current_class() +``` + +- `run_current_method` - Run the test method on the cursor + +```lua +require('java').test.run_current_method() +``` + +- `debug_current_method` - Debug the test method on the cursor + +```lua +require('java').test.debug_current_method() +``` + +- `view_report` - Open the last test report in a popup window + +```lua +require('java').test.view_last_report() +``` + +### Profiles + +```lua +require('java').profile.ui() +``` + +### Refactor + +- `extract_variable` - Create a variable from value at cursor/selection + +```lua +require('java').refactor.extract_variable() +``` + +- `extract_variable_all_occurrence` - Create a variable for all occurrences from + value at cursor/selection + +```lua +require('java').refactor.extract_variable_all_occurrence() +``` + +- `extract_constant` - Create a constant from the value at cursor/selection + +```lua +require('java').refactor.extract_constant() +``` + +- `extract_method` - Create method from the value at cursor/selection + +```lua +require('java').refactor.extract_method() +``` + +- `extract_field` - Create a field from the value at cursor/selection + +```lua +require('java').refactor.extract_field() +``` + +### Settings + +- `change_runtime` - Change the JDK version to another + +```lua +require('java').settings.change_runtime() +``` + +
+ +## :clamp: How to Use JDK X.X Version? + +
+ +:small_orange_diamond:details + +### Method 1 + +[Neoconf](https://github.com/folke/neoconf.nvim) can be used to manage LSP +setting including jdtls. Neoconf allows global configuration as well as project-wise +configurations. Here is how you can set Jdtls setting on `neoconf.json` + +```json +{ + "lspconfig": { + "jdtls": { + "java.configuration.runtimes": [ + { + "name": "JavaSE-21", + "path": "/opt/jdk-21", + "default": true + } + ] + } + } +} +``` + +### Method 2 + +Pass the settings to Jdtls setup. + +```lua +require('lspconfig').jdtls.setup({ + settings = { + java = { + configuration = { + runtimes = { + { + name = "JavaSE-21", + path = "/opt/jdk-21", + default = true, + } + } + } + } + } +}) +``` + +
+ +## :wrench: Configuration + +
+ +:small_orange_diamond:details + +For most users changing the default configuration is not necessary. But if you +want, following options are available + +```lua +{ + -- list of file that exists in root of the project + root_markers = { + 'settings.gradle', + 'settings.gradle.kts', + 'pom.xml', + 'build.gradle', + 'mvnw', + 'gradlew', + 'build.gradle', + 'build.gradle.kts', + '.git', + }, + + jdtls = { + version = 'v1.43.0', + }, + + lombok = { + version = 'nightly', + }, + + -- load java test plugins + java_test = { + enable = true, + version = '0.40.1', + }, + + -- load java debugger plugins + java_debug_adapter = { + enable = true, + version = '0.58.1', + }, + + spring_boot_tools = { + enable = true, + version = '1.55.1', + }, + + jdk = { + -- install jdk using mason.nvim + auto_install = true, + version = '17.0.2', + }, + + notifications = { + -- enable 'Configuring DAP' & 'DAP configured' messages on start up + dap = true, + }, + + -- We do multiple verifications to make sure things are in place to run this + -- plugin + verification = { + -- nvim-java checks for the order of execution of following + -- * require('java').setup() + -- * require('lspconfig').jdtls.setup() + -- IF they are not executed in the correct order, you will see a error + -- notification. + -- Set following to false to disable the notification if you know what you + -- are doing + invalid_order = true, + + -- nvim-java checks if the require('java').setup() is called multiple + -- times. + -- IF there are multiple setup calls are executed, an error will be shown + -- Set following property value to false to disable the notification if + -- you know what you are doing + duplicate_setup_calls = true, + + -- nvim-java checks if nvim-java/mason-registry is added correctly to + -- mason.nvim plugin. + -- IF it's not registered correctly, an error will be thrown and nvim-java + -- will stop setup + invalid_mason_registry = false, + }, + + mason = { + -- These mason registries will be prepended to the existing mason + -- configuration + registries = { + 'github:nvim-java/mason-registry', + }, + }, +} + +``` + +
+ +## :golf: Architecture + +
+ +:small_orange_diamond:details + +Following is the high level idea. Jdtls is the language server nvim-java +communicates with. However, we don't have all the features we need just in +Jdtls. So, we are loading java-test & java-debug-adapter extensions when we +launch Jdtls. Once the language server is started, we communicate with the +language server to do stuff. + +For instance, to run the current test, + +- Request Jdtls for test classes +- Request Jdtls for class paths, module paths, java executable +- Request Jdtls to start a debug session and send the port of the session back +- Prepare TCP connections to listen to the test results +- Start nvim-dap and let user interactions to be handled by nvim-dap +- Parse the test results as they come in +- Once the execution is done, open a window show the test results + +```text + ┌────────────┐ ┌────────────┐ + │ │ │ │ + │ Neovim │ │ VSCode │ + │ │ │ │ + └─────▲──────┘ └──────▲─────┘ + │ │ + │ │ + │ │ + │ │ +┌───────▼───────┐ ┌──────────────▼──────────────┐ +│ │ │ │ +│ nvim-java │ │ Extension Pack for Java │ +│ │ │ │ +└───────▲───────┘ └──────────────▲──────────────┘ + │ │ + │ │ + │ │ + │ │ + │ │ + │ ┌───────────┐ │ + │ │ │ │ + └──────────────► JDTLS ◄────────────┘ + │ │ + └───▲───▲───┘ + │ │ + │ │ + │ │ + │ │ + │ │ + ┌───────────────┐ │ │ ┌────────────────────────┐ + │ │ │ │ │ │ + │ java-test ◄────────┘ └─────────► java-debug-adapter │ + │ │ │ │ + └───────────────┘ └────────────────────────┘ +``` + +
+ +## :bookmark_tabs: Projects Acknowledgement + +- [spring-boot.nvim](https://github.com/JavaHello/spring-boot.nvim) is the one + that starts sts4 & do other necessary `jdtls` `sts4` sync command registration + in `nvim-java`. + +- [nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls) is a plugin that follows + "Keep it simple, stupid!" approach. If you love customizing things by yourself, + then give nvim-jdtls a try. + +> [!WARNING] +> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` +> before installing this From 4fea5726a6c5198143cfd9450df8fa6e78838398 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 19:05:37 +0530 Subject: [PATCH 11/27] chore: disable line length check in luacheck --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index 33c63e5..232e3ba 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -15,3 +15,4 @@ read_globals = { ignore = { '212/self', } +max_line_length = false From ee276050468cdd2776f5cb74360f0935d787bd2d Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 19:06:26 +0530 Subject: [PATCH 12/27] chore(ci): enable debug logging in CI tests --- tests/utils/prepare-config.lua | 18 +++++++++++++++--- tests/utils/test-config.lua | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/utils/prepare-config.lua b/tests/utils/prepare-config.lua index 544f21e..ef1bfc2 100644 --- a/tests/utils/prepare-config.lua +++ b/tests/utils/prepare-config.lua @@ -31,11 +31,23 @@ require('lazy').setup({ 'nvim-java/nvim-java', dir = '.', config = function() - require('java').setup({ + local is_nixos = vim.fn.filereadable('/etc/NIXOS') == 1 + local is_ci = vim.env.CI ~= nil + + local config = { jdk = { - auto_install = false, + auto_install = not is_nixos, }, - }) + } + + if is_ci then + config.log = { + level = 'debug', + use_console = true, + } + end + + require('java').setup(config) vim.lsp.enable('jdtls') end, }, diff --git a/tests/utils/test-config.lua b/tests/utils/test-config.lua index bd1fb1f..c2cca10 100644 --- a/tests/utils/test-config.lua +++ b/tests/utils/test-config.lua @@ -18,11 +18,21 @@ vim.opt.runtimepath:append(temp_path .. '/nvim-dap') vim.opt.runtimepath:append('.') local is_nixos = vim.fn.filereadable('/etc/NIXOS') == 1 +local is_ci = vim.env.CI ~= nil -require('java').setup({ +local config = { jdk = { auto_install = not is_nixos, }, -}) +} + +if is_ci then + config.log = { + level = 'debug', + use_console = true, + } +end + +require('java').setup(config) vim.lsp.enable('jdtls') From 83f8b86f0a5ef5ccfa078c5aad3108894f8ae88d Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 19:06:47 +0530 Subject: [PATCH 13/27] docs: update README with native lsp config info --- README.md | 5 +---- lua/java-test/api.lua | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c540b14..4ffb603 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,6 @@ Just install and start writing `public static void main(String[] args)`. -> [!CAUTION] -> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` before installing this - > [!TIP] > You can find cool tips & tricks here https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks @@ -35,7 +32,7 @@ Just install and start writing `public static void main(String[] args)`. ## :bulb: Why - Everything necessary will be installed automatically -- Uses [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) to setup `jdtls` +- Uses native `vim.lsp.config` to setup `jdtls` - Realtime server settings updates is possible using [neoconf](https://github.com/folke/neoconf.nvim) - Auto loads necessary `jdtls` plugins - Supported plugins are, diff --git a/lua/java-test/api.lua b/lua/java-test/api.lua index f222a7f..48fe2b5 100644 --- a/lua/java-test/api.lua +++ b/lua/java-test/api.lua @@ -94,7 +94,9 @@ function M:run_test(tests, report, config) local launch_args = self.test_client:resolve_junit_launch_arguments(test_adapters.tests_to_junit_launch_params(tests)) - log.debug('resolved launch args - mainClass: ' .. launch_args.mainClass .. ', projectName: ' .. launch_args.projectName) + log.debug( + 'resolved launch args - mainClass: ' .. launch_args.mainClass .. ', projectName: ' .. launch_args.projectName + ) local java_exec = self.debug_client:resolve_java_executable(launch_args.mainClass, launch_args.projectName) From 3886f2557f3d19aea94b434d0946ec537c3af160 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 19:11:54 +0530 Subject: [PATCH 14/27] docs: clarify feat: usage in commit messages --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d716f17..65aa678 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,5 +149,8 @@ log.fatal('fatal message') ## Git Guidelines - Use conventional commit messages per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +- `feat:` is ONLY for end-user features (e.g., `feat: add code completion`) + - CI/internal features use `chore(ci):` (e.g., `chore(ci): enable debug logs`) + - Build/tooling features use `chore(build):`, `chore(test):`, etc. - Never append "generated by AI" message - Split unrelated changes into separate commits From 97e433125c7446aa136ddc83d26660997aa5d72a Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 22:19:04 +0530 Subject: [PATCH 15/27] docs: update installation instructions for native LSP --- README.md | 163 ++++++++++++++++++++---------------------------------- 1 file changed, 60 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 4ffb603..3193795 100644 --- a/README.md +++ b/README.md @@ -57,26 +57,40 @@ You can click on **n commits ahead of** link to see the changes made on top of t - [Kickstart](https://github.com/nvim-java/starter-kickstart) - [AstroNvim](https://github.com/nvim-java/starter-astronvim) -### Custom Configuration Instructions +### Manual Installation Instructions -- Install the plugin +**Requirements:** Neovim 0.11+ -Using [lazy.nvim](https://github.com/folke/lazy.nvim) +#### Using `vim.pack` ```lua -return {'nvim-java/nvim-java'} -``` +vim.pack.add({ + { + src = 'https://github.com/JavaHello/spring-boot.nvim', + version = '218c0c26c14d99feca778e4d13f5ec3e8b1b60f0', + }, + 'https://github.com/MunifTanjim/nui.nvim', + 'https://github.com/mfussenegger/nvim-dap', -- Setup nvim-java before `lspconfig` + 'https://github.com/nvim-java/nvim-java', +}) -```lua require('java').setup() +vim.lsp.enable('jdtls') ``` -- Setup jdtls like you would usually do +#### Using `lazy.nvim` + +Install using [lazy.nvim](https://github.com/folke/lazy.nvim): ```lua -require('lspconfig').jdtls.setup({}) +{ + 'nvim-java/nvim-java', + config = function() + require('java').setup() + vim.lsp.enable('jdtls') + end, +} ``` Yep! That's all :) @@ -282,42 +296,20 @@ require('java').settings.change_runtime() :small_orange_diamond:details -### Method 1 - -[Neoconf](https://github.com/folke/neoconf.nvim) can be used to manage LSP -setting including jdtls. Neoconf allows global configuration as well as project-wise -configurations. Here is how you can set Jdtls setting on `neoconf.json` - -```json -{ - "lspconfig": { - "jdtls": { - "java.configuration.runtimes": [ - { - "name": "JavaSE-21", - "path": "/opt/jdk-21", - "default": true - } - ] - } - } -} -``` - -### Method 2 - -Pass the settings to Jdtls setup. +Use `vim.lsp.config()` to override the default JDTLS settings: ```lua -require('lspconfig').jdtls.setup({ - settings = { - java = { - configuration = { - runtimes = { - { - name = "JavaSE-21", - path = "/opt/jdk-21", - default = true, +vim.lsp.config('jdtls', { + default_config = { + settings = { + java = { + configuration = { + runtimes = { + { + name = "JavaSE-21", + path = "/opt/jdk-21", + default = true, + } } } } @@ -335,41 +327,35 @@ require('lspconfig').jdtls.setup({ :small_orange_diamond:details For most users changing the default configuration is not necessary. But if you -want, following options are available +want, following options are available: ```lua -{ - -- list of file that exists in root of the project - root_markers = { - 'settings.gradle', - 'settings.gradle.kts', - 'pom.xml', - 'build.gradle', - 'mvnw', - 'gradlew', - 'build.gradle', - 'build.gradle.kts', - '.git', +require('java').setup({ + -- Startup checks + checks = { + nvim_version = true, -- Check Neovim version + nvim_jdtls_conflict = true, -- Check for nvim-jdtls conflict }, + -- JDTLS configuration jdtls = { - version = 'v1.43.0', + version = '1.43.0', }, + -- Extensions lombok = { - version = 'nightly', + enable = true, + version = '1.18.40', }, - -- load java test plugins java_test = { enable = true, version = '0.40.1', }, - -- load java debugger plugins java_debug_adapter = { enable = true, - version = '0.58.1', + version = '0.58.2', }, spring_boot_tools = { @@ -377,52 +363,27 @@ want, following options are available version = '1.55.1', }, + -- JDK installation jdk = { - -- install jdk using mason.nvim auto_install = true, - version = '17.0.2', + version = '17', }, + -- Notifications notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, + dap = true, -- Show DAP configuration messages }, - -- We do multiple verifications to make sure things are in place to run this - -- plugin - verification = { - -- nvim-java checks for the order of execution of following - -- * require('java').setup() - -- * require('lspconfig').jdtls.setup() - -- IF they are not executed in the correct order, you will see a error - -- notification. - -- Set following to false to disable the notification if you know what you - -- are doing - invalid_order = true, - - -- nvim-java checks if the require('java').setup() is called multiple - -- times. - -- IF there are multiple setup calls are executed, an error will be shown - -- Set following property value to false to disable the notification if - -- you know what you are doing - duplicate_setup_calls = true, - - -- nvim-java checks if nvim-java/mason-registry is added correctly to - -- mason.nvim plugin. - -- IF it's not registered correctly, an error will be thrown and nvim-java - -- will stop setup - invalid_mason_registry = false, + -- Logging + log = { + use_console = true, + use_file = true, + level = 'info', + log_file = vim.fn.stdpath('state') .. '/nvim-java.log', + max_lines = 1000, + show_location = false, }, - - mason = { - -- These mason registries will be prepended to the existing mason - -- configuration - registries = { - 'github:nvim-java/mason-registry', - }, - }, -} - +}) ``` @@ -497,7 +458,3 @@ For instance, to run the current test, - [nvim-jdtls](https://github.com/mfussenegger/nvim-jdtls) is a plugin that follows "Keep it simple, stupid!" approach. If you love customizing things by yourself, then give nvim-jdtls a try. - -> [!WARNING] -> You cannot use `nvim-java` alongside `nvim-jdtls`. So remove `nvim-jdtls` -> before installing this From 5ad9a9428b2edfb58a5cf2fb7db8d39219937ca5 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 22:23:42 +0530 Subject: [PATCH 16/27] fix: update nvim version check to 0.11 --- lua/java/checks/nvim-version.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/java/checks/nvim-version.lua b/lua/java/checks/nvim-version.lua index 300c932..a18325f 100644 --- a/lua/java/checks/nvim-version.lua +++ b/lua/java/checks/nvim-version.lua @@ -7,11 +7,11 @@ function M:run(config) return end - if vim.fn.has('nvim-0.11.5') ~= 1 then + if vim.fn.has('nvim-0.11') ~= 1 then local err = require('java-core.utils.errors') err.throw([[ - nvim-java is only tested on Neovim 0.11.5 or greater - Please upgrade to Neovim 0.11.5 or greater. + nvim-java is only tested on Neovim 0.11 or greater + Please upgrade to Neovim 0.11 or greater. If you are sure it works on your version, disable the version check: checks = { nvim_version = false }' ]]) From 51690a894d5b223ba105bb985a4f0f63b76bd9b3 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 22:46:59 +0530 Subject: [PATCH 17/27] chore(ci): add lsp log output for mac tests --- tests/specs/lsp_spec.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/specs/lsp_spec.lua b/tests/specs/lsp_spec.lua index 8ef81b7..a8c349d 100644 --- a/tests/specs/lsp_spec.lua +++ b/tests/specs/lsp_spec.lua @@ -1,8 +1,13 @@ local lsp_utils = dofile('tests/utils/lsp-utils.lua') +local system = require('java-core.utils.system') local assert = require('luassert') describe('LSP Attach', function() it('should attach when opening a Java buffer', function() + if system.get_os() == 'mac' then + vim.print(vim.fn.readfile('/Users/runner/.local/state/nvim/lsp.log')) + end + vim.cmd.edit('HelloWorld.java') local jdtls = lsp_utils.wait_for_lsp_attach('jdtls', 30000) From c0cf386948efa3c7a8fd774e1f622ee4f317a7c0 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 23:10:19 +0530 Subject: [PATCH 18/27] fix: nvim-java does not work on mac with the embeded jdk --- lua/java-core/constants/java_version.lua | 2 +- lua/java-core/ls/servers/jdtls/env.lua | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lua/java-core/constants/java_version.lua b/lua/java-core/constants/java_version.lua index 4dc8c05..d8c8459 100644 --- a/lua/java-core/constants/java_version.lua +++ b/lua/java-core/constants/java_version.lua @@ -1,5 +1,5 @@ return { - ['1.43.0'] = { from = 17, to = 21 }, + ['1.43.0'] = { from = 17, to = 17 }, ['1.44.0'] = { from = 17, to = 21 }, ['1.45.0'] = { from = 21, to = 21 }, ['1.46.0'] = { from = 21, to = 21 }, diff --git a/lua/java-core/ls/servers/jdtls/env.lua b/lua/java-core/ls/servers/jdtls/env.lua index 3e1bed2..6fcf040 100644 --- a/lua/java-core/ls/servers/jdtls/env.lua +++ b/lua/java-core/ls/servers/jdtls/env.lua @@ -1,6 +1,7 @@ local path = require('java-core.utils.path') local Manager = require('pkgm.manager') local log = require('java-core.utils.log2') +local system = require('java-core.utils.system') --- @TODO: importing stuff from java main package feels wrong. --- We should fix this in the future @@ -16,7 +17,15 @@ function M.get_env(opts) end local jdk_root = Manager:get_install_dir('openjdk', config.jdk.version) - local java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) + + local java_home + + if system.get_os() == 'mac' then + java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*', 'Contents', 'Home')) + else + java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) + end + local java_bin = path.join(java_home, 'bin') local env = { From dc090d5d2502b9d9141825be11597dad9b83e895 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Sun, 30 Nov 2025 23:35:44 +0530 Subject: [PATCH 19/27] fix: java validation is invalid --- lua/java-core/ls/servers/jdtls/cmd.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/java-core/ls/servers/jdtls/cmd.lua b/lua/java-core/ls/servers/jdtls/cmd.lua index 371c358..2be1cfc 100644 --- a/lua/java-core/ls/servers/jdtls/cmd.lua +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -98,16 +98,16 @@ end ---@private function M.validate_java_version() - local v = M.get_java_major_version() + local curr_ver = M.get_java_major_version() local exp_ver = java_version_map[conf.jdtls.version] - if v <= exp_ver.from and v >= exp_ver.to then + if not (curr_ver >= exp_ver.to and curr_ver <= exp_ver.from) then local msg = string.format( - 'Java version mismatch: JDTLS %s requires Java %d - %d, but found Java %d', + 'Java version mismatch: JDTLS %s requires Java %d <= java >= %d, but found Java %d', conf.jdtls.version, exp_ver.from, exp_ver.to, - v + curr_ver ) err.throw(msg) From dbb175505021ae1175a2b8c9a03afef27b231fef Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Mon, 1 Dec 2025 01:22:51 +0530 Subject: [PATCH 20/27] chore: remove unwanted code --- tests/specs/lsp_spec.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/specs/lsp_spec.lua b/tests/specs/lsp_spec.lua index a8c349d..8ef81b7 100644 --- a/tests/specs/lsp_spec.lua +++ b/tests/specs/lsp_spec.lua @@ -1,13 +1,8 @@ local lsp_utils = dofile('tests/utils/lsp-utils.lua') -local system = require('java-core.utils.system') local assert = require('luassert') describe('LSP Attach', function() it('should attach when opening a Java buffer', function() - if system.get_os() == 'mac' then - vim.print(vim.fn.readfile('/Users/runner/.local/state/nvim/lsp.log')) - end - vim.cmd.edit('HelloWorld.java') local jdtls = lsp_utils.wait_for_lsp_attach('jdtls', 30000) From e2ce4b918750fac8c9af5111d96d920d7301f0e6 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Mon, 1 Dec 2025 01:36:06 +0530 Subject: [PATCH 21/27] fix: java version check does not consider env passed for LSP --- lua/java-core/ls/servers/jdtls/cmd.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lua/java-core/ls/servers/jdtls/cmd.lua b/lua/java-core/ls/servers/jdtls/cmd.lua index 2be1cfc..39d1e6b 100644 --- a/lua/java-core/ls/servers/jdtls/cmd.lua +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -20,7 +20,7 @@ function M.get_cmd(opts) return function(dispatchers, config) local cmd = M.get_jvm_args(opts):concat(M.get_jar_args()) - M.validate_java_version() + M.validate_java_version(config.cmd_env) log.debug('Starting jdtls with cmd', cmd) @@ -97,8 +97,9 @@ function M.get_jar_args(cwd) end ---@private -function M.validate_java_version() - local curr_ver = M.get_java_major_version() +---@param env table +function M.validate_java_version(env) + local curr_ver = M.get_java_major_version(env) local exp_ver = java_version_map[conf.jdtls.version] if not (curr_ver >= exp_ver.to and curr_ver <= exp_ver.from) then @@ -115,8 +116,11 @@ function M.validate_java_version() end ---@private -function M.get_java_major_version() - local version = vim.fn.system('java -version') +---@param env table +function M.get_java_major_version(env) + local proc = vim.system({ 'java', '-version' }, { env = env }):wait() + local version = proc.stderr or proc.stdout or '' + local major = version:match('version (%d+)') or version:match('version "(%d+)') or version:match('openjdk (%d+)') From 14e37220f34c11d6ebbf22b124d7565ec96a18f3 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Mon, 1 Dec 2025 01:41:22 +0530 Subject: [PATCH 22/27] fix: PATH env separator is not windows compatible --- lua/java-core/ls/servers/jdtls/env.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/java-core/ls/servers/jdtls/env.lua b/lua/java-core/ls/servers/jdtls/env.lua index 6fcf040..128e5f6 100644 --- a/lua/java-core/ls/servers/jdtls/env.lua +++ b/lua/java-core/ls/servers/jdtls/env.lua @@ -28,8 +28,10 @@ function M.get_env(opts) local java_bin = path.join(java_home, 'bin') + local separator = system.get_os() == 'win' and ';' or ':' + local env = { - ['PATH'] = java_bin .. ':' .. vim.fn.getenv('PATH'), + ['PATH'] = java_bin .. separator .. vim.fn.getenv('PATH'), ['JAVA_HOME'] = java_home, } From 68ade552868f4a6b1141c87dd9741fa3ef4b6aa7 Mon Sep 17 00:00:00 2001 From: Srinesh Nisala Date: Wed, 3 Dec 2025 22:22:54 +0530 Subject: [PATCH 23/27] fix: windows compatibility issue (#439) --- .devcontainer/config/nvim/lazy-lock.json | 3 +-- lua/java-core/ls/servers/jdtls/cmd.lua | 28 ++++++++++++++++++++++-- lua/pkgm/downloaders/powershell.lua | 5 ++++- lua/pkgm/extractors/tar.lua | 20 ++++++++++++++++- tests/specs/lsp_spec.lua | 4 ++++ 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.devcontainer/config/nvim/lazy-lock.json b/.devcontainer/config/nvim/lazy-lock.json index 73cbd4c..3737ab9 100644 --- a/.devcontainer/config/nvim/lazy-lock.json +++ b/.devcontainer/config/nvim/lazy-lock.json @@ -1,7 +1,6 @@ { "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, - "nvim-dap": { "branch": "master", "commit": "b38f7d30366d9169d0a623c4c85fbcf99d8d58bb" }, - "nvim-jdtls": { "branch": "master", "commit": "943e2398aba6b7e976603708450c6c93c600e830" }, + "nvim-dap": { "branch": "master", "commit": "5860c7c501eb428d3137ee22c522828d20cca0b3" }, "spring-boot.nvim": { "branch": "main", "commit": "218c0c26c14d99feca778e4d13f5ec3e8b1b60f0" } } diff --git a/lua/java-core/ls/servers/jdtls/cmd.lua b/lua/java-core/ls/servers/jdtls/cmd.lua index 39d1e6b..8ac1691 100644 --- a/lua/java-core/ls/servers/jdtls/cmd.lua +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -20,7 +20,14 @@ function M.get_cmd(opts) return function(dispatchers, config) local cmd = M.get_jvm_args(opts):concat(M.get_jar_args()) - M.validate_java_version(config.cmd_env) + -- NOTE: eventhough we are setting the PATH env var, due to a bug, it's not + -- working on Windows. So just lanching 'java' will result in executing the + -- system java. So as a workaround, we use the absolute path to java instead + -- So following check is not needed when we have auto_install set to true + -- @see https://github.com/neovim/neovim/issues/36818 + if not conf.jdk.auto_install then + M.validate_java_version(config.cmd_env) + end log.debug('Starting jdtls with cmd', cmd) @@ -40,8 +47,25 @@ end function M.get_jvm_args(opts) local jdtls_config = path.join(jdtls_root, system.get_config_suffix()) + local java_exe = 'java' + + -- NOTE: eventhough we are setting the PATH env var, due to a bug, it's not + -- working on Windows. So we are using the absolute path to java instead + -- @see https://github.com/neovim/neovim/issues/36818 + if conf.jdk.auto_install then + local jdk_root = Manager:get_install_dir('openjdk', conf.jdk.version) + local java_home + if system.get_os() == 'mac' then + java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*', 'Contents', 'Home')) + else + java_home = vim.fn.glob(path.join(jdk_root, 'jdk-*')) + end + + java_exe = path.join(java_home, 'bin', 'java') + end + local jvm_args = List:new({ - 'java', + java_exe, '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Declipse.product=org.eclipse.jdt.ls.core.product', diff --git a/lua/pkgm/downloaders/powershell.lua b/lua/pkgm/downloaders/powershell.lua index e2236d7..f0a5434 100644 --- a/lua/pkgm/downloaders/powershell.lua +++ b/lua/pkgm/downloaders/powershell.lua @@ -1,6 +1,7 @@ local class = require('java-core.utils.class') local log = require('java-core.utils.log2') local err_util = require('java-core.utils.errors') +local path = require('java-core.utils.path') ---@class java-core.PowerShell ---@field url string @@ -21,7 +22,9 @@ function PowerShell:_init(opts) if not opts.dest then local filename = vim.fs.basename(opts.url) - self.dest = vim.fn.tempname() .. '-' .. filename + local tmp_dir = vim.fn.tempname() + vim.fn.mkdir(tmp_dir, 'p') + self.dest = path.join(tmp_dir, filename) log.debug('Using temp destination:', self.dest) else self.dest = opts.dest diff --git a/lua/pkgm/extractors/tar.lua b/lua/pkgm/extractors/tar.lua index 58c873b..3d88036 100644 --- a/lua/pkgm/extractors/tar.lua +++ b/lua/pkgm/extractors/tar.lua @@ -17,6 +17,18 @@ function Tar:_init(opts) self.dest = opts.dest end +---@private +---Check if tar supports --force-local +---@param tar_cmd string +---@return boolean +function Tar:tar_supports_force_local(tar_cmd) + local ok, out = pcall(vim.fn.system, { tar_cmd, '--help' }) + if not ok then + return false + end + return out:match('%-%-force%-local') ~= nil +end + ---Extract tar file using tar ---@return boolean|nil # true on success, nil on failure ---@return string|nil # Error message if failed @@ -30,7 +42,13 @@ function Tar:extract() -- Windows: convert backslashes to forward slashes (tar accepts them) local source = self.source:gsub('\\', '/') local dest = self.dest:gsub('\\', '/') - cmd = string.format('%s --no-same-owner --force-local -xf "%s" -C "%s"', tar_cmd, source, dest) + cmd = string.format( + '%s --no-same-owner %s -xf "%s" -C "%s"', + tar_cmd, + self:tar_supports_force_local(tar_cmd) and '--force-local' or '', + source, + dest + ) else -- Unix: use shellescape cmd = string.format( diff --git a/tests/specs/lsp_spec.lua b/tests/specs/lsp_spec.lua index 8ef81b7..459de77 100644 --- a/tests/specs/lsp_spec.lua +++ b/tests/specs/lsp_spec.lua @@ -1,5 +1,6 @@ local lsp_utils = dofile('tests/utils/lsp-utils.lua') local assert = require('luassert') +local system = require('java-core.utils.system') describe('LSP Attach', function() it('should attach when opening a Java buffer', function() @@ -8,6 +9,9 @@ describe('LSP Attach', function() local jdtls = lsp_utils.wait_for_lsp_attach('jdtls', 30000) local spring = lsp_utils.wait_for_lsp_attach('spring-boot', 30000) + if system.get_os() == 'win' then + vim.fn.readfile('C:/Users/runneradmin/AppData/Local/nvim-data/lsp.log') + end assert.is_not_nil(jdtls, 'JDTLS should attach to Java buffer') assert.is_not_nil(spring, 'Spring Boot should attach to Java buffer') end) From ca68ebd802fab02ccc1dcb0f87884a02a12e40fd Mon Sep 17 00:00:00 2001 From: Srinesh Nisala Date: Thu, 4 Dec 2025 03:26:32 +0530 Subject: [PATCH 24/27] chore(doc): update docs (#440) --- README.md | 15 +++++++++------ doc/nvim-java.txt | 7 +------ lua/java/config.lua | 7 ------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3193795..05f4289 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ ![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white) ![Lua](https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white) +![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) +![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows11&logoColor=white) +![macOS](https://img.shields.io/badge/macOS-000000?style=for-the-badge&logo=apple&logoColor=white) + +--- + Just install and start writing `public static void main(String[] args)`. > [!TIP] @@ -27,13 +33,15 @@ Just install and start writing `public static void main(String[] args)`. - :white_check_mark: Organize Imports & Code Formatting - :white_check_mark: Running Tests - :white_check_mark: Run & Debug Profiles +- :white_check_mark: Built-in Application Runner with Log Viewer +- :white_check_mark: Profile Management UI +- :white_check_mark: Decompiler Support - :white_check_mark: [Code Actions](https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks#running-code-actions) ## :bulb: Why - Everything necessary will be installed automatically - Uses native `vim.lsp.config` to setup `jdtls` -- Realtime server settings updates is possible using [neoconf](https://github.com/folke/neoconf.nvim) - Auto loads necessary `jdtls` plugins - Supported plugins are, - `spring-boot-tools` @@ -369,11 +377,6 @@ require('java').setup({ version = '17', }, - -- Notifications - notifications = { - dap = true, -- Show DAP configuration messages - }, - -- Logging log = { use_console = true, diff --git a/doc/nvim-java.txt b/doc/nvim-java.txt index 31ad3be..d2bebe8 100644 --- a/doc/nvim-java.txt +++ b/doc/nvim-java.txt @@ -407,12 +407,7 @@ want, following options are available auto_install = true, version = '17.0.2', }, - - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, - }, - + -- We do multiple verifications to make sure things are in place to run this -- plugin verification = { diff --git a/lua/java/config.lua b/lua/java/config.lua index 8c9439d..b42419a 100644 --- a/lua/java/config.lua +++ b/lua/java/config.lua @@ -20,7 +20,6 @@ local V = jdtls_version_map[JDTLS_VERSION] ---@field java_debug_adapter { enable: boolean, version: string } ---@field spring_boot_tools { enable: boolean, version: string } ---@field jdk { auto_install: boolean, version: string } ----@field notifications { dap: boolean } ---@field log java-core.Log2Config ---@class java.PartialConfig @@ -31,7 +30,6 @@ local V = jdtls_version_map[JDTLS_VERSION] ---@field java_debug_adapter? { enable?: boolean, version?: string } ---@field spring_boot_tools? { enable?: boolean, version?: string } ---@field jdk? { auto_install?: boolean, version?: string } ----@field notifications? { dap?: boolean } ---@field log? java-core.PartialLog2Config ---@type java.Config @@ -72,11 +70,6 @@ local config = { version = V.jdk, }, - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, - }, - log = { use_console = true, use_file = true, From 105f2348d9292a2dac236d9b8b247d3707b93393 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Thu, 4 Dec 2025 03:42:53 +0530 Subject: [PATCH 25/27] docs: remove starter configs, simplify install structure --- README.md | 16 ++-------------- doc/nvim-java.txt | 12 ------------ 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 05f4289..b10b0b3 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,9 @@ Just install and start writing `public static void main(String[] args)`. :small_orange_diamond:details -### Starter Configs (Recommend for newbies) - -Following are forks of original repositories pre-configured for java. If you -don't know how to get started, use one of the following to get started. -You can click on **n commits ahead of** link to see the changes made on top of the original project - -- [LazyVim](https://github.com/nvim-java/starter-lazyvim) -- [Kickstart](https://github.com/nvim-java/starter-kickstart) -- [AstroNvim](https://github.com/nvim-java/starter-astronvim) - -### Manual Installation Instructions - **Requirements:** Neovim 0.11+ -#### Using `vim.pack` +### Using `vim.pack` ```lua vim.pack.add({ @@ -87,7 +75,7 @@ require('java').setup() vim.lsp.enable('jdtls') ``` -#### Using `lazy.nvim` +### Using `lazy.nvim` Install using [lazy.nvim](https://github.com/folke/lazy.nvim): diff --git a/doc/nvim-java.txt b/doc/nvim-java.txt index d2bebe8..6ab22ff 100644 --- a/doc/nvim-java.txt +++ b/doc/nvim-java.txt @@ -68,18 +68,6 @@ HOW TO INSTALL *nvim-java-how-to-install* :small_orange_diamond:details ~ -STARTER CONFIGS (RECOMMEND FOR NEWBIES) ~ - -Following are forks of original repositories pre-configured for java. If you -don’t know how to get started, use one of the following to get started. You -can click on **n commits ahead of** link to see the changes made on top of the -original project - -- LazyVim -- Kickstart -- AstroNvim - - CUSTOM CONFIGURATION INSTRUCTIONS ~ - Install the plugin From 71d3a83433495651e9af338aef70a15bc9b0a682 Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Thu, 4 Dec 2025 03:43:05 +0530 Subject: [PATCH 26/27] chore: release 4.0.0 Release-As: 4.0.0 From cdbcecc59dff5c47935869d2c6d8f0263f02e2ad Mon Sep 17 00:00:00 2001 From: s1n7ax Date: Thu, 4 Dec 2025 03:48:00 +0530 Subject: [PATCH 27/27] docs: remove why section --- README.md | 16 ++-------------- doc/nvim-java.txt | 14 -------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b10b0b3..3110ad2 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,11 @@ Just install and start writing `public static void main(String[] args)`. +--- + > [!TIP] > You can find cool tips & tricks here https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks -> [!NOTE] -> If you are facing errors while using, please check troubleshoot wiki https://github.com/nvim-java/nvim-java/wiki/Troubleshooting - ## :loudspeaker: Demo @@ -38,17 +37,6 @@ Just install and start writing `public static void main(String[] args)`. - :white_check_mark: Decompiler Support - :white_check_mark: [Code Actions](https://github.com/nvim-java/nvim-java/wiki/Tips-&-Tricks#running-code-actions) -## :bulb: Why - -- Everything necessary will be installed automatically -- Uses native `vim.lsp.config` to setup `jdtls` -- Auto loads necessary `jdtls` plugins - - Supported plugins are, - - `spring-boot-tools` - - `lombok` - - `java-test` - - `java-debug-adapter` - ## :hammer: How to Install
diff --git a/doc/nvim-java.txt b/doc/nvim-java.txt index 6ab22ff..72a3ccc 100644 --- a/doc/nvim-java.txt +++ b/doc/nvim-java.txt @@ -6,7 +6,6 @@ Table of Contents *nvim-java-table-of-contents* 1. nvim-java |nvim-java-nvim-java| - Demo |nvim-java-demo| - Features |nvim-java-features| - - Why |nvim-java-why| - How to Install |nvim-java-how-to-install| - Commands |nvim-java-commands| - APIs |nvim-java-apis| @@ -50,19 +49,6 @@ FEATURES *nvim-java-features* - Code Actions -WHY *nvim-java-why* - -- Everything necessary will be installed automatically -- Uses nvim-lspconfig to setup `jdtls` -- Realtime server settings updates is possible using neoconf -- Auto loads necessary `jdtls` plugins - - Supported plugins are, - - `spring-boot-tools` - - `lombok` - - `java-test` - - `java-debug-adapter` - - HOW TO INSTALL *nvim-java-how-to-install* :small_orange_diamond:details ~