diff --git a/.devcontainer/config/nvim/init.lua b/.devcontainer/config/nvim/init.lua new file mode 100644 index 0000000..b13bd64 --- /dev/null +++ b/.devcontainer/config/nvim/init.lua @@ -0,0 +1,170 @@ +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({ + jdk = { + auto_install = false, + }, + log = { + use_console = false, + level = 'debug', + }, + }) + 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, + }, + { + 'ibhagwan/fzf-lua', + -- optional for icon support + dependencies = { 'nvim-tree/nvim-web-devicons' }, + -- or if using mini.icons/mini.nvim + -- dependencies = { "nvim-mini/mini.icons" }, + ---@module "fzf-lua" + ---@type fzf-lua.Config|{} + ---@diagnostics disable: missing-fields + opts = {}, + ---@diagnostics enable: missing-fields + }, +}) + +-- 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.opt.number = true +vim.opt.relativenumber = true + +local k = vim.keymap.set + +k('n', '', 'q') + +if colemak then + k('n', '', '') + k('n', 'E', 'K') + k('n', 'H', 'I') + k('n', 'K', 'N') + k('n', 'L', 'E') + k('n', 'N', 'J') + k('n', 'e', '') + k('n', 'h', 'i') + k('n', 'i', '') + k('n', 'j', 'm') + k('n', 'k', 'n') + k('n', 'l', 'e') + k('n', 'm', '') + k('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 }) + k('i', '', function() + vim.lsp.completion.get() + end, { buffer = args.buf }) + + if colemak then + k('i', '', '', { buffer = args.buf }) + k('i', '', '', { buffer = args.buf }) + end + end, +}) + +k('n', ']d', function() + vim.diagnostic.jump({ count = 1, float = true }) +end, { desc = 'Jump to next diagnostic' }) + +k('n', '[d', function() + vim.diagnostic.jump({ count = -1, float = true }) +end, { desc = 'Jump to previous diagnostic' }) + +k('n', 'ta', vim.lsp.buf.code_action, {}) + +-- DAP keymaps +k('n', 'dd', function() + require('dap').toggle_breakpoint() +end, { desc = 'Toggle breakpoint' }) + +k('n', 'dc', function() + require('dap').continue() +end, { desc = 'Continue' }) + +k('n', 'dn', function() + require('dap').step_over() +end, { desc = 'Step over' }) + +k('n', 'di', function() + require('dap').step_into() +end, { desc = 'Step into' }) + +k('n', 'do', function() + require('dap').step_out() +end, { desc = 'Step out' }) + +k('n', 'dr', function() + require('dap').repl.open() +end, { desc = 'Open REPL' }) + +k('n', 'dl', function() + require('dap').run_last() +end, { desc = 'Run last' }) + +k('n', 'dt', function() + require('dap').terminate() +end, { desc = 'Terminate' }) + +k('n', 'gd', function() + vim.lsp.buf.definition() +end, { desc = 'Terminate' }) + +k('n', 'tt', function() + require('fzf-lua').lsp_document_symbols() +end) + +k('n', 'm', "vnewput = execute('messages')") + +k('n', 'nn', 'JavaRunnerRunMain', { desc = 'Run main' }) +k('n', 'ne', 'JavaRunnerStopMain', { desc = 'Stop main' }) +k('n', 'nt', 'JavaTestDebugCurrentClass', { desc = 'Debug test' }) +k('n', 'ns', 'JavaTestRunCurrentClass', { desc = 'Run test' }) + +k('t', 'yy', '', { desc = 'Exit terminal mode' }) + +k('n', '', 'j', { desc = 'Window down' }) +k('n', '', 'k', { desc = 'Window up' }) +k('n', '', 'h', { desc = 'Window left' }) +k('n', '', 'l', { desc = 'Window right' }) diff --git a/.devcontainer/config/nvim/lazy-lock.json b/.devcontainer/config/nvim/lazy-lock.json new file mode 100644 index 0000000..933e059 --- /dev/null +++ b/.devcontainer/config/nvim/lazy-lock.json @@ -0,0 +1,8 @@ +{ + "fzf-lua": { "branch": "main", "commit": "de0fd4a21ee29cf6532d0c3bcae08a0b25d99b6a" }, + "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, + "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, + "nvim-dap": { "branch": "master", "commit": "5860c7c501eb428d3137ee22c522828d20cca0b3" }, + "nvim-web-devicons": { "branch": "master", "commit": "8dcb311b0c92d460fac00eac706abd43d94d68af" }, + "spring-boot.nvim": { "branch": "main", "commit": "218c0c26c14d99feca778e4d13f5ec3e8b1b60f0" } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3fc365c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "$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/features/node: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": {}, + "ghcr.io/devcontainers-extra/features/fzf:1": {} + }, + "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/docs.yml b/.github/workflows/docs.yml index 96bd2bf..13d15ec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,9 @@ on: push: branches: - main + paths: + - README.md + - .github/workflows/panvimdoc.yml permissions: pull-requests: write @@ -20,11 +23,11 @@ jobs: with: vimdoc: "nvim-java" dedupsubheadings: false - version: "Neovim >= 0.9.4" + version: "Neovim >= 0.11.5" demojify: true - name: create pull request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: base: "main" commit-message: "chore(doc): automatic vimdoc update" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 11e3060..b29c1e1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,8 @@ name: Lint on: push: + branches: + - "main" pull_request: jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca11efb..5ee0126 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,6 @@ jobs: name: release runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v4 + - uses: googleapis/release-please-action@v4 with: release-type: simple diff --git a/.github/workflows/stylua.yml b/.github/workflows/stylua.yml index a7b609a..79840b2 100644 --- a/.github/workflows/stylua.yml +++ b/.github/workflows/stylua.yml @@ -2,6 +2,8 @@ name: Stylua on: push: + branches: + - "main" pull_request: jobs: @@ -9,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: JohnnyMorganz/stylua-action@v3 + - uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} version: latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb1e389..9875bb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,16 +2,19 @@ name: Test on: push: + branches: + - "main" pull_request: 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 @@ -22,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 c5cdeab..232e3ba 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -4,6 +4,7 @@ globals = { 'vim.wo', 'vim.bo', 'vim.opt', + 'vim.lsp', } read_globals = { 'vim', @@ -11,3 +12,7 @@ read_globals = { 'it', 'assert', } +ignore = { + '212/self', +} +max_line_length = false 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/CHANGELOG.md b/CHANGELOG.md index fb7ad98..b07db49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,263 @@ # Changelog +## [4.1.0](https://github.com/nvim-java/nvim-java/compare/v4.0.4...v4.1.0) (2026-02-04) + + +### Features + +* add curl downloader ([#473](https://github.com/nvim-java/nvim-java/issues/473)) ([d4050eb](https://github.com/nvim-java/nvim-java/commit/d4050ebc7ed909dcf4b536c109e8ce291112efc3)) +* add JavaTestRunAllTests and JavaTestDebugAllTests commands ([#467](https://github.com/nvim-java/nvim-java/issues/467)) ([e4cc304](https://github.com/nvim-java/nvim-java/commit/e4cc304fa7a5daf41c13d6894bc90b4e99e576e6)) + +## [4.0.4](https://github.com/nvim-java/nvim-java/compare/v4.0.3...v4.0.4) (2025-12-11) + + +### Bug Fixes + +* **dap:** allow running mains without project ([#459](https://github.com/nvim-java/nvim-java/issues/459)) ([9b6c907](https://github.com/nvim-java/nvim-java/commit/9b6c907e2616a8ada8419d7b3e68e85056ba5d57)) + +## [4.0.3](https://github.com/nvim-java/nvim-java/compare/v4.0.2...v4.0.3) (2025-12-10) + + +### Bug Fixes + +* **doc:** invalid JDTLS settings in README ([#451](https://github.com/nvim-java/nvim-java/issues/451)) ([df1ff5f](https://github.com/nvim-java/nvim-java/commit/df1ff5f85df8d4b7b8d92215084d7c183520893f)) + +## [4.0.2](https://github.com/nvim-java/nvim-java/compare/v4.0.1...v4.0.2) (2025-12-10) + + +### Bug Fixes + +* vim.lsp.buf.document_symbol returns nothing for jdtls ([#449](https://github.com/nvim-java/nvim-java/issues/449)) ([625f48f](https://github.com/nvim-java/nvim-java/commit/625f48ffedc735ac6284316e16daa99ba012a996)) + +## [4.0.1](https://github.com/nvim-java/nvim-java/compare/v4.0.0...v4.0.1) (2025-12-05) + + +### Bug Fixes + +* user config is not applied due to default config import in some modules ([#443](https://github.com/nvim-java/nvim-java/issues/443)) ([7875237](https://github.com/nvim-java/nvim-java/commit/787523792f71816d049772d4ffc16ded6bac497f)) + +## [4.0.0](https://github.com/nvim-java/nvim-java/compare/v3.0.0...v4.0.0) (2025-12-03) + + +### Features + +* add jproperties filetype support ([56e82af](https://github.com/nvim-java/nvim-java/commit/56e82afd514a47b28437b158dd43332814f08523)) +* v4.0.0 ([84b9253](https://github.com/nvim-java/nvim-java/commit/84b92531b1de55ee9bd1d0614a05e9965481d386)) + + +### Bug Fixes + +* java validation is invalid ([ad5d370](https://github.com/nvim-java/nvim-java/commit/ad5d3701ea007720960062110ea11d588e4f3311)) +* java version check does not consider env passed for LSP ([41d72bf](https://github.com/nvim-java/nvim-java/commit/41d72bf9fb9ec8d6edf8cdaea2cee970276d6c55)) +* nvim-java does not work on mac with the embeded jdk ([293ee31](https://github.com/nvim-java/nvim-java/commit/293ee310b4c51c14d7571be24117c09025f320bf)) +* PATH env separator is not windows compatible ([9743dde](https://github.com/nvim-java/nvim-java/commit/9743ddefa6e0c1af78cfa5094658231215918794)) +* update nvim version check to 0.11 ([c3e3129](https://github.com/nvim-java/nvim-java/commit/c3e31292f731f281f6294adb543b9943a8331593)) +* windows compatibility issue ([#439](https://github.com/nvim-java/nvim-java/issues/439)) ([e1bb01d](https://github.com/nvim-java/nvim-java/commit/e1bb01db00f20be381eb9937a8517a347fa57bf6)) +* workspace_execute calling client command handler ([20191fe](https://github.com/nvim-java/nvim-java/commit/20191fe610d0c11dc12e7a6310f18bd56ea68d4d)) + + +### Miscellaneous Chores + +* release 4.0.0 ([47f3ea4](https://github.com/nvim-java/nvim-java/commit/47f3ea45db76563c54f8876358c226eeebdd2705)) + +## [3.0.0](https://github.com/nvim-java/nvim-java/compare/v2.1.2...v3.0.0) (2025-08-06) + + +### Features + +* [@logrusx](https://github.com/logrusx) adds Mason 2.0 support ([#402](https://github.com/nvim-java/nvim-java/issues/402)) ([58c25cd](https://github.com/nvim-java/nvim-java/commit/58c25cd45d867fc512af48a457c71bb26d9d778d)) + + +### Miscellaneous Chores + +* release 3.0.0 ([3cecf73](https://github.com/nvim-java/nvim-java/commit/3cecf7362e5f83e4a34e84e4f14c65bad81968f1)) + +## [2.1.2](https://github.com/nvim-java/nvim-java/compare/v2.1.1...v2.1.2) (2025-08-04) + + +### Bug Fixes + +* java-debug-adapter doean't install ([#407](https://github.com/nvim-java/nvim-java/issues/407)) ([2776094](https://github.com/nvim-java/nvim-java/commit/2776094c745af0d99b5acb24a4594d85b4f99545)) +* mason registry verification fails ([#405](https://github.com/nvim-java/nvim-java/issues/405)) ([4fd68c4](https://github.com/nvim-java/nvim-java/commit/4fd68c4025acaafb9efbf2e0cf69e017bcc4476c)) +* **typo:** project vise -> project-wise ([#390](https://github.com/nvim-java/nvim-java/issues/390)) ([7c2e81c](https://github.com/nvim-java/nvim-java/commit/7c2e81caa301b0d1bc7992b88981af883b3b5d6b)) + +## [2.1.1](https://github.com/nvim-java/nvim-java/compare/v2.1.0...v2.1.1) (2025-02-16) + + +### Bug Fixes + +* nvim-java mason reg is not added if the mason is not merging parent config in user end ([#355](https://github.com/nvim-java/nvim-java/issues/355)) ([db54fbf](https://github.com/nvim-java/nvim-java/commit/db54fbf6a022f2720f86c6b6d7383ba501211b80)) + +## [2.1.0](https://github.com/nvim-java/nvim-java/compare/v2.0.2...v2.1.0) (2025-01-26) + + +### Features + +* add sync ui select util ([#345](https://github.com/nvim-java/nvim-java/issues/345)) ([c616f72](https://github.com/nvim-java/nvim-java/commit/c616f72fa2ea0ad9d7798e1d7cab56aa6de56108)) + + +### Bug Fixes + +* **dap:** do not override previously defined user config ([#342](https://github.com/nvim-java/nvim-java/issues/342)) ([4d810a5](https://github.com/nvim-java/nvim-java/commit/4d810a546c262ca8f60228dc98ba51f81f5649c6)) + +## [2.0.2](https://github.com/nvim-java/nvim-java/compare/v2.0.1...v2.0.2) (2024-12-24) + + +### Bug Fixes + +* runner cmd [#241](https://github.com/nvim-java/nvim-java/issues/241) ([#329](https://github.com/nvim-java/nvim-java/issues/329)) ([a36f50c](https://github.com/nvim-java/nvim-java/commit/a36f50c82f922f352d4ce7ac6a3c6b238b3e0a36)) + +## [2.0.1](https://github.com/nvim-java/nvim-java/compare/v2.0.0...v2.0.1) (2024-08-01) + + +### Bug Fixes + +* refactor and build lua API being registered incorrectly ([#284](https://github.com/nvim-java/nvim-java/issues/284)) ([b9e6b71](https://github.com/nvim-java/nvim-java/commit/b9e6b71c8cbb3f6db8ce7d3a9bd3f3cb805156f1)) + +## [2.0.0](https://github.com/nvim-java/nvim-java/compare/v1.21.0...v2.0.0) (2024-07-25) + + +### ⚠ BREAKING CHANGES + +* move all the client commands to nvim-refactor repo ([#278](https://github.com/nvim-java/nvim-java/issues/278)) + +### Code Refactoring + +* move all the client commands to nvim-refactor repo ([#278](https://github.com/nvim-java/nvim-java/issues/278)) ([1c04d72](https://github.com/nvim-java/nvim-java/commit/1c04d72d10a4807583096848dc6ad92192a94ee1)) + +## [1.21.0](https://github.com/nvim-java/nvim-java/compare/v1.20.0...v1.21.0) (2024-07-15) + + +### Features + +* adding delegate method generate code action ([9462546](https://github.com/nvim-java/nvim-java/commit/94625466f5023719c3625438fcf95f75f7d0c02d)) + +## [1.20.0](https://github.com/nvim-java/nvim-java/compare/v1.19.0...v1.20.0) (2024-07-14) + + +### Features + +* add generate hash code and equals code action ([6a714fe](https://github.com/nvim-java/nvim-java/commit/6a714fedc4a8a327d2acebe34d673b768dcbb0d8)) + +## [1.19.0](https://github.com/nvim-java/nvim-java/compare/v1.18.0...v1.19.0) (2024-07-14) + + +### Features + +* add clean workspace command ([8a1171c](https://github.com/nvim-java/nvim-java/commit/8a1171cc21aaae58f8a08759562814aea87e694d)) +* add toString code action ([4b1e1bd](https://github.com/nvim-java/nvim-java/commit/4b1e1bd5206174b8914858d085fe6809482b5575)) + +## [1.18.0](https://github.com/nvim-java/nvim-java/compare/v1.17.0...v1.18.0) (2024-07-14) + + +### Features + +* add warning on not yet implemented client commands ([a889ff4](https://github.com/nvim-java/nvim-java/commit/a889ff4ab49774849bd647d136370fa1b69c70f8)) + +## [1.17.0](https://github.com/nvim-java/nvim-java/compare/v1.16.0...v1.17.0) (2024-07-13) + + +### Features + +* add generate constructor code action ([ea5371b](https://github.com/nvim-java/nvim-java/commit/ea5371bf0de96dd6856ae623455376d6e2062045)) + +## [1.16.0](https://github.com/nvim-java/nvim-java/compare/v1.15.0...v1.16.0) (2024-07-13) + + +### Features + +* add convert to variable refactor command ([2635a64](https://github.com/nvim-java/nvim-java/commit/2635a640aebd007b52a3d288b1318cacca7dc44c)) + +## [1.15.0](https://github.com/nvim-java/nvim-java/compare/v1.14.0...v1.15.0) (2024-07-12) + + +### Features + +* add extract_field command ([aabca01](https://github.com/nvim-java/nvim-java/commit/aabca011b56b605f7bf12df0534f7d5b60b16fff)) + +## [1.14.0](https://github.com/nvim-java/nvim-java/compare/v1.13.0...v1.14.0) (2024-07-11) + + +### Features + +* upgrade java debug adapter ([644c4cb](https://github.com/nvim-java/nvim-java/commit/644c4cbe7cda5d7bac08f7ec293e08078d4afac0)) + +## [1.13.0](https://github.com/nvim-java/nvim-java/compare/v1.12.0...v1.13.0) (2024-07-10) + + +### Features + +* add more extract commands ([0ec0f46](https://github.com/nvim-java/nvim-java/commit/0ec0f463efa7b3cc77d30660ced357e295ab7cd7)) + +## [1.12.0](https://github.com/nvim-java/nvim-java/compare/v1.11.0...v1.12.0) (2024-07-10) + + +### Features + +* add command to change the runtime ([#244](https://github.com/nvim-java/nvim-java/issues/244)) ([af9c8ff](https://github.com/nvim-java/nvim-java/commit/af9c8ff3c7cf313611daa194409cb65e7831e98a)) + + +### Bug Fixes + +* remove github token from stylua workflow ([a6b1c8b](https://github.com/nvim-java/nvim-java/commit/a6b1c8b8a5569476c1a73bcb606ba2e33025d54e)) +* the manually stoped/restarted job show the error message ([#242](https://github.com/nvim-java/nvim-java/issues/242)) ([#243](https://github.com/nvim-java/nvim-java/issues/243)) ([0b9fac9](https://github.com/nvim-java/nvim-java/commit/0b9fac9cae5ac13590d5e8201d9611aebbbece73)) + +## [1.11.0](https://github.com/nvim-java/nvim-java/compare/v1.10.0...v1.11.0) (2024-07-06) + + +### Features + +* add build workspace command ([4d92c3d](https://github.com/nvim-java/nvim-java/commit/4d92c3d8552aa3c80a3f4a98754e570a564addf5)) + +## [1.10.0](https://github.com/nvim-java/nvim-java/compare/v1.9.1...v1.10.0) (2024-07-05) + + +### Features + +* add spring boot tools support ([#232](https://github.com/nvim-java/nvim-java/issues/232)) ([028e870](https://github.com/nvim-java/nvim-java/commit/028e870c5a69cf5ee68e4776e9614a664cc65871)) + +## [1.9.1](https://github.com/nvim-java/nvim-java/compare/v1.9.0...v1.9.1) (2024-07-05) + + +### Bug Fixes + +* get_client func is failing on older neovim ([bb7d586](https://github.com/nvim-java/nvim-java/commit/bb7d586161bf3e10153dc6a1180984d310c025fe)) + +## [1.9.0](https://github.com/nvim-java/nvim-java/compare/v1.8.0...v1.9.0) (2024-07-03) + + +### Features + +* add mason registry check ([#225](https://github.com/nvim-java/nvim-java/issues/225)) ([ef7597d](https://github.com/nvim-java/nvim-java/commit/ef7597d158f0687c8c0bd2c11e5907c32e52574f)) + +## [1.8.0](https://github.com/nvim-java/nvim-java/compare/v1.7.0...v1.8.0) (2024-07-01) + + +### Features + +* add validations for exec order, duplicate setup calls ([#219](https://github.com/nvim-java/nvim-java/issues/219)) ([15bc822](https://github.com/nvim-java/nvim-java/commit/15bc822acb1e11983bde70f436dd17d41ba76925)) + +## [1.7.0](https://github.com/nvim-java/nvim-java/compare/v1.6.1...v1.7.0) (2024-06-28) + + +### Features + +* add lazy.lua file to declare dependencies on lazy.nvim ([#215](https://github.com/nvim-java/nvim-java/issues/215)) ([1349ac5](https://github.com/nvim-java/nvim-java/commit/1349ac545feb37459a04a0a37d41496463c63c87)) + +## [1.6.1](https://github.com/nvim-java/nvim-java/compare/v1.6.0...v1.6.1) (2024-06-27) + + +### Bug Fixes + +* when the same main class is ran again, first process is not stopped ([1fae8de](https://github.com/nvim-java/nvim-java/commit/1fae8de1327167ac4b9f744970b2b6b7c7652874)) + +## [1.6.0](https://github.com/nvim-java/nvim-java/compare/v1.5.1...v1.6.0) (2024-06-25) + + +### Features + +* handle multiple running app ([#182](https://github.com/nvim-java/nvim-java/issues/182)) ([#191](https://github.com/nvim-java/nvim-java/issues/191)) ([ca5cfdb](https://github.com/nvim-java/nvim-java/commit/ca5cfdba0d0629a829d16fa838808be0d5db8baa)) + ## [1.5.1](https://github.com/nvim-java/nvim-java/compare/v1.5.0...v1.5.1) (2024-05-29) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..65aa678 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,156 @@ +# 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/) +- `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 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 c94b909..d7776b2 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,41 @@ # :coffee: nvim-java +![Spring](https://img.shields.io/badge/Spring-6DB33F?style=for-the-badge&logo=spring&logoColor=white) ![Java](https://img.shields.io/badge/java-%23ED8B00.svg?style=for-the-badge&logo=openjdk&logoColor=white) ![Gradle](https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white) ![Apache Maven](https://img.shields.io/badge/Apache%20Maven-C71A36?style=for-the-badge&logo=Apache%20Maven&logoColor=white) ![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)`. -> [!WARNING] -> 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 ## :loudspeaker: Demo -https://github.com/nvim-java/nvim-java/assets/18459807/047c8c46-9a0a-4869-b342-d5c2e15647bc + ## :dizzy: Features +- :white_check_mark: Spring Boot Tools - :white_check_mark: Diagnostics & Auto Completion -- :white_check_mark: Automatic [DAP](https://github.com/mfussenegger/nvim-dap) - debug configuration -- :white_check_mark: Running tests -- :white_check_mark: Run & Debug profiles - -## :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, - - `lombok` - - `java-test` - - `java-debug-adapter` +- :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: 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) ## :hammer: How to Install @@ -40,55 +43,38 @@ https://github.com/nvim-java/nvim-java/assets/18459807/047c8c46-9a0a-4869-b342-d :small_orange_diamond:details -### Q & A - -If you face any issues, check our [Q & A](https://github.com/nvim-java/nvim-java/wiki/Q-&-A) wiki to see if that helps - -### Distributions - -- [Lazyvim](https://github.com/nvim-java/nvim-java/wiki/Lazyvim) +**Requirements:** Neovim 0.11.5+ -### Custom - -- Install the plugin - -Using [lazy.nvim](https://github.com/folke/lazy.nvim) +### Using `vim.pack` ```lua -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', - { - 'williamboman/mason.nvim', - opts = { - registries = { - 'github:nvim-java/mason-registry', - 'github:mason-org/mason-registry', - }, - }, - } +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 :) @@ -101,6 +87,14 @@ Yep! That's all :) :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 @@ -125,6 +119,8 @@ Yep! That's all :) - `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 +- `JavaTestRunAllTests` - Run all tests in the workspace +- `JavaTestDebugAllTests` - Debug all tests in the workspace - `JavaTestViewLastReport` - Open the last test report in a popup window ### Profiles @@ -133,7 +129,16 @@ Yep! That's all :) ### Refactor -- `JavaRefactorExtractVariable` - Create a variable from returned value at cursor +- `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 @@ -143,6 +148,22 @@ Yep! That's all :) :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 @@ -200,6 +221,18 @@ require('java').test.run_current_method() require('java').test.debug_current_method() ``` +- `run_all_tests` - Run all tests in the workspace + +```lua +require('java').test.run_all_tests() +``` + +- `debug_all_tests` - Debug all tests in the workspace + +```lua +require('java').test.debug_all_tests() +``` + - `view_report` - Open the last test report in a popup window ```lua @@ -214,48 +247,57 @@ require('java').profile.ui() ### Refactor -- `extract_variable` - Create a variable from returned value at cursor +- `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 -## :clamp: How to Use JDK X.X Version? +```lua +require('java').refactor.extract_variable_all_occurrence() +``` -
- -:small_orange_diamond:details +- `extract_constant` - Create a constant from the value at cursor/selection -### Method 1 +```lua +require('java').refactor.extract_constant() +``` -[Neoconf](https://github.com/folke/neoconf.nvim) can be used to manage LSP -setting including jdtls. Neoconf allows global configuration as well as project -vice configurations. Here is how you can set Jdtls setting on `neoconf.json` +- `extract_method` - Create method from the value at cursor/selection -```json -{ - "lspconfig": { - "jdtls": { - "java.configuration.runtimes": [ - { - "name": "JavaSE-21", - "path": "/opt/jdk-21", - "default": true - } - ] - } - } -} +```lua +require('java').refactor.extract_method() ``` -### Method 2 +- `extract_field` - Create a field from the value at cursor/selection -Pass the settings to Jdtls setup. +```lua +require('java').refactor.extract_field() +``` + +### Settings + +- `change_runtime` - Change the JDK version to another ```lua -require('lspconfig').jdtls.setup({ +require('java').settings.change_runtime() +``` + +
+ +## :clamp: How to Use JDK X.X Version? + +
+ +:small_orange_diamond:details + +Use `vim.lsp.config()` to override the default JDTLS settings: + +```lua +vim.lsp.config('jdtls', { settings = { java = { configuration = { @@ -281,43 +323,58 @@ 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', - }, - - -- load java test plugins - java_test = { - enable = true, - }, - - -- load java debugger plugins - java_debug_adapter = { - enable = true, - }, - - jdk = { - -- install jdk using mason.nvim - auto_install = true, - }, - - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, - }, -} +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 = '1.43.0', + }, + + -- Extensions + lombok = { + enable = true, + version = '1.18.40', + }, + + java_test = { + enable = true, + version = '0.40.1', + }, + + java_debug_adapter = { + enable = true, + version = '0.58.2', + }, + + spring_boot_tools = { + enable = true, + version = '1.55.1', + }, + + -- JDK installation + jdk = { + auto_install = true, + version = '17', + }, + + -- 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, + }, +}) ```
@@ -385,7 +442,10 @@ For instance, to run the current test, ## :bookmark_tabs: Projects Acknowledgement -[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. I may or may not have copied some code :wink: -Beauty of Open source! +- [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. 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/nvim-java.txt b/doc/nvim-java.txt index c740642..709c13b 100644 --- a/doc/nvim-java.txt +++ b/doc/nvim-java.txt @@ -1,4 +1,4 @@ -*nvim-java.txt* For Neovim >= 0.9.4 Last change: 2024 May 04 +*nvim-java.txt* For Neovim >= 0.11.5 Last change: 2026 February 05 ============================================================================== Table of Contents *nvim-java-table-of-contents* @@ -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| @@ -21,86 +20,74 @@ Table of Contents *nvim-java-table-of-contents* + + +------------------------------------------------------------------------------ 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 DEMO *nvim-java-demo* -https://github.com/nvim-java/nvim-java/assets/18459807/047c8c46-9a0a-4869-b342-d5c2e15647bc + FEATURES *nvim-java-features* +- Spring Boot Tools - Diagnostics & Auto Completion -- Automatic DAP - debug configuration -- Running tests - - -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, - - `lombok` - - `java-test` - - `java-debug-adapter` +- Automatic Debug Configuration +- Organize Imports & Code Formatting +- Running Tests +- Run & Debug Profiles +- Built-in Application Runner with Log Viewer +- Profile Management UI +- Decompiler Support +- Code Actions HOW TO INSTALL *nvim-java-how-to-install* :small_orange_diamond:details ~ +**Requirements:** Neovim 0.11.5+ -DISTRIBUTIONS ~ -- Lazyvim - - -CUSTOM ~ - -- Install the plugin - -Using lazy.nvim +USING VIM.PACK ~ >lua - 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', - { - 'williamboman/mason.nvim', - opts = { - registries = { - 'github:nvim-java/mason-registry', - 'github:mason-org/mason-registry', - }, - }, - } + 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', + + 'https://github.com/nvim-java/nvim-java', + }) + + require('java').setup() + vim.lsp.enable('jdtls') < -- Setup nvim-java before `lspconfig` ->lua - require('java').setup() -< +USING LAZY.NVIM ~ -- Setup jdtls like you would usually do +Install using 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 :) @@ -111,6 +98,13 @@ COMMANDS *nvim-java-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 @@ -137,6 +131,8 @@ TEST ~ - `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 +- `JavaTestRunAllTests` - Run all tests in the workspace +- `JavaTestDebugAllTests` - Debug all tests in the workspace - `JavaTestViewLastReport` - Open the last test report in a popup window @@ -147,7 +143,17 @@ PROFILES ~ REFACTOR ~ -- `JavaRefactorExtractVariable` - Create a variable from returned value at cursor +- `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 APIS *nvim-java-apis* @@ -155,6 +161,23 @@ APIS *nvim-java-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 @@ -214,6 +237,18 @@ TEST ~ require('java').test.debug_current_method() < +- `run_all_tests` - Run all tests in the workspace + +>lua + require('java').test.run_all_tests() +< + +- `debug_all_tests` - Debug all tests in the workspace + +>lua + require('java').test.debug_all_tests() +< + - `view_report` - Open the last test report in a popup window >lua @@ -230,47 +265,55 @@ PROFILES ~ REFACTOR ~ -- `extract_variable` - Create a variable from returned value at cursor +- `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 -HOW TO USE JDK X.X VERSION? *nvim-java-how-to-use-jdk-x.x-version?* +>lua + require('java').refactor.extract_variable_all_occurrence() +< -:small_orange_diamond:details ~ +- `extract_constant` - Create a constant from the value at cursor/selection +>lua + require('java').refactor.extract_constant() +< -METHOD 1 ~ +- `extract_method` - Create method from the value at cursor/selection -Neoconf can be used to manage LSP -setting including jdtls. Neoconf allows global configuration as well as project -vice configurations. Here is how you can set Jdtls setting on `neoconf.json` +>lua + require('java').refactor.extract_method() +< ->json - { - "lspconfig": { - "jdtls": { - "java.configuration.runtimes": [ - { - "name": "JavaSE-21", - "path": "/opt/jdk-21", - "default": true - } - ] - } - } - } +- `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() < -METHOD 2 ~ +HOW TO USE JDK X.X VERSION? *nvim-java-how-to-use-jdk-x.x-version?* -Pass the settings to Jdtls setup. +:small_orange_diamond:details ~ + +Use `vim.lsp.config()` to override the default JDTLS settings: >lua - require('lspconfig').jdtls.setup({ + vim.lsp.config('jdtls', { settings = { java = { configuration = { @@ -293,43 +336,58 @@ CONFIGURATION *nvim-java-configuration* :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 + }, - -- load java test plugins - java_test = { - enable = true, - }, + -- JDTLS configuration + jdtls = { + version = '1.43.0', + }, - -- load java debugger plugins - java_debug_adapter = { - enable = true, - }, + -- Extensions + lombok = { + enable = true, + version = '1.18.40', + }, - jdk = { - -- install jdk using mason.nvim - auto_install = true, - }, + java_test = { + enable = true, + version = '0.40.1', + }, - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, - }, - } + java_debug_adapter = { + enable = true, + version = '0.58.2', + }, + + spring_boot_tools = { + enable = true, + version = '1.55.1', + }, + + -- JDK installation + jdk = { + auto_install = true, + version = '17', + }, + + -- 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, + }, + }) < @@ -393,19 +451,25 @@ For instance, to run the current test, PROJECTS ACKNOWLEDGEMENT *nvim-java-projects-acknowledgement* -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. I may or may not have copied some code -Beautyof Open source! +- spring-boot.nvim is the one + that starts sts4 & do other necessary `jdtls` `sts4` sync command registration + in `nvim-java`. +- 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. ============================================================================== 2. Links *nvim-java-links* -1. *Java*: https://img.shields.io/badge/java-%23ED8B00.svg?style=for-the-badge&logo=openjdk&logoColor=white -2. *Gradle*: https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white -3. *Apache Maven*: https://img.shields.io/badge/Apache%20Maven-C71A36?style=for-the-badge&logo=Apache%20Maven&logoColor=white -4. *Neovim*: https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white -5. *Lua*: https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white +1. *Spring*: https://img.shields.io/badge/Spring-6DB33F?style=for-the-badge&logo=spring&logoColor=white +2. *Java*: https://img.shields.io/badge/java-%23ED8B00.svg?style=for-the-badge&logo=openjdk&logoColor=white +3. *Gradle*: https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white +4. *Apache Maven*: https://img.shields.io/badge/Apache%20Maven-C71A36?style=for-the-badge&logo=Apache%20Maven&logoColor=white +5. *Neovim*: https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white +6. *Lua*: https://img.shields.io/badge/lua-%232C2D72.svg?style=for-the-badge&logo=lua&logoColor=white +7. *Linux*: https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black +8. *Windows*: https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows11&logoColor=white +9. *macOS*: https://img.shields.io/badge/macOS-000000?style=for-the-badge&logo=apple&logoColor=white Generated by panvimdoc 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 new file mode 100644 index 0000000..5e21aec --- /dev/null +++ b/lazy.lua @@ -0,0 +1,11 @@ +return { + 'nvim-java/nvim-java', + dependencies = { + 'MunifTanjim/nui.nvim', + 'mfussenegger/nvim-dap', + { + 'JavaHello/spring-boot.nvim', + commit = '218c0c26c14d99feca778e4d13f5ec3e8b1b60f0', + }, + }, +} 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..2fc2de4 --- /dev/null +++ b/lua/java-core/constants/java_version.lua @@ -0,0 +1,4 @@ +return { + ['1.43.0'] = { from = 17, to = 17 }, + ['1.54.0'] = { from = 21, to = 25 }, +} 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..98ecc01 --- /dev/null +++ b/lua/java-core/ls/clients/jdtls-client.lua @@ -0,0 +1,381 @@ +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, params, buffer) + return self:request('workspace/executeCommand', { + command = command, + arguments = params, + }, buffer) +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..bc63ec2 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/cmd.lua @@ -0,0 +1,177 @@ +local List = require('java-core.utils.list') +local path = require('java-core.utils.path') +local Manager = require('pkgm.manager') +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 str = require('java-core.utils.str') + +local M = {} + +--- Returns a function that returns the command to start jdtls +---@param config java.Config +function M.get_cmd(config) + ---@param dispatchers? vim.lsp.rpc.Dispatchers + ---@param lsp_config vim.lsp.ClientConfig + return function(dispatchers, lsp_config) + local cmd = M.get_jvm_args(config):concat(M.get_jar_args(config)) + + -- 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 config.jdk.auto_install then + M.validate_java_version(config, lsp_config.cmd_env) + end + + log.debug('Starting jdtls with cmd', cmd) + + local result = vim.lsp.rpc.start(cmd, dispatchers, { + cwd = lsp_config.cmd_cwd, + env = lsp_config.cmd_env, + detached = lsp_config.detached, + }) + + return result + end +end + +---@private +---@param config java.Config +---@return java-core.List +function M.get_jvm_args(config) + local use_lombok = config.lombok.enable + local jdtls_root = Manager:get_install_dir('jdtls', config.jdtls.version) + 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 config.jdk.auto_install then + local jdk_root = Manager:get_install_dir('openjdk', config.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_exe, + '-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 use_lombok then + local lombok_root = Manager:get_install_dir('lombok', config.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 config java.Config +---@param cwd? string +---@return java-core.List +function M.get_jar_args(config, cwd) + local jdtls_root = Manager:get_install_dir('jdtls', config.jdtls.version) + 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 +---@param config java.Config +---@param env table +function M.validate_java_version(config, env) + local curr_ver = M.get_java_major_version(env) + local exp_ver = java_version_map[config.jdtls.version] + + if not exp_ver then + err.throw( + str.multiline( + 'We maintain a jdlts to java version map to provide a better error message.', + 'The jdtls version you are using is not supported yet', + 'Please open an issue on https://github.com/nvim-java/nvim-java/issues', + 'OR submit a PR', + 'Version map:', + 'https://github.com/nvim-java/nvim-java/blob/main/lua/java-core/constants/java_version.lua' + ) + ) + end + + if not (curr_ver >= exp_ver.from and curr_ver <= exp_ver.to) then + local msg = string.format( + 'Java version mismatch: JDTLS %s requires Java %d <= java >= %d, but found Java %d', + config.jdtls.version, + exp_ver.from, + exp_ver.to, + curr_ver + ) + + err.throw(msg) + end +end + +---@private +---@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+)') + 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..0cd37c1 --- /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 = false, + 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..2c6ab81 --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/env.lua @@ -0,0 +1,39 @@ +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') + +local M = {} + +--- @param config java.Config +function M.get_env(config) + if not config.jdk.auto_install then + log.debug('config.jdk.auto_install disabled, returning empty env') + return {} + end + + local jdk_root = Manager:get_install_dir('openjdk', config.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 + + local java_bin = path.join(java_home, 'bin') + + local separator = system.get_os() == 'win' and ';' or ':' + + local env = { + ['PATH'] = java_bin .. separator .. 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/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 new file mode 100644 index 0000000..dfa263d --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/init.lua @@ -0,0 +1,29 @@ +local M = {} + +--- Returns jdtls config +---@param opts { plugins: string[], config: java.Config } +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 filetype = require('java-core.ls.servers.jdtls.filetype') + 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.config) + base_conf.cmd_env = env.get_env(opts.config) + base_conf.init_options.bundles = plugins.get_plugins(opts.config, opts.plugins) + base_conf.root_markers = root.get_root_markers() + base_conf.filetypes = filetype.get_filetypes() + + log.debug('jdtls config', base_conf) + + 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..a66ce5b --- /dev/null +++ b/lua/java-core/ls/servers/jdtls/plugins.lua @@ -0,0 +1,50 @@ +local M = {} + +function M.get_plugin_version_map(config) + return { + ['java-test'] = config.java_test.version, + ['java-debug'] = config.java_debug_adapter.version, + ['spring-boot-tools'] = config.spring_boot_tools.version, + } +end + +---Returns a list of .jar file paths for given list of jdtls plugins +---@param config java.Config +---@param plugins string[] +---@return string[] # list of .jar file paths +function M.get_plugins(config, plugins) + local file = require('java-core.utils.file') + local List = require('java-core.utils.list') + local Manager = require('pkgm.manager') + local path = require('java-core.utils.path') + local err = require('java-core.utils.errors') + local str = require('java-core.utils.str') + + local plugin_version_map = M.get_plugin_version_map(config) + + return List:new(plugins) + :map(function(plugin_name) + local version = plugin_version_map[plugin_name] + + local pkg_path = Manager:get_install_dir(plugin_name, version) + local plugin_root = path.join(pkg_path, 'extension') + local package_json_str = vim.fn.readfile(path.join(plugin_root, 'package.json')) + local package_json = vim.json.decode(table.concat(package_json_str, '\n')) + local java_extensions = package_json.contributes.javaExtensions + + local ext_jars = file.resolve_paths(plugin_root, java_extensions) + + if #ext_jars ~= #java_extensions then + err.throw( + str + .multiline('Failed to load some jars for "%s"', 'Expected %d jars but only %d found') + :format(plugin_name, #java_extensions, #ext_jars) + ) + end + + return ext_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..9291b1d --- /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 ... string error message +function M.throw(...) + notify.error(...) + log.error(...) + error(...) +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/str.lua b/lua/java-core/utils/str.lua new file mode 100644 index 0000000..e9ff7ba --- /dev/null +++ b/lua/java-core/utils/str.lua @@ -0,0 +1,10 @@ +local M = {} + +--- Joins a list of strings with a separator +---@param ... string +---@return string +function M.multiline(...) + return table.concat({ ... }, '\n') +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..cf73550 --- /dev/null +++ b/lua/java-dap/setup.lua @@ -0,0 +1,131 @@ +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) + + -- skip enriching if already enriched + if config.mainClass and config.projectName and config.modulePaths and config.classPaths and config.javaExec then + return config + end + + local main = config.mainClass + -- when we set it to empty string, it will create a project with some random + -- string as name + local project = config.projectName or '' + + assert(main, 'To enrich the config, mainClass 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 new file mode 100644 index 0000000..96ed4cc --- /dev/null +++ b/lua/java-runner/run-logger.lua @@ -0,0 +1,57 @@ +local class = require('java-core.utils.class') + +---@class java.RunLogger +---@field window number +local RunLogger = class() + +function RunLogger:_init() + self.window = -1 +end + +---Opens the log window with the given run buffer +---@param buffer number +function RunLogger:create(buffer) + vim.cmd('sp | winc J | res 15 | buffer ' .. buffer) + self.window = vim.api.nvim_get_current_win() + + vim.wo[self.window].number = false + vim.wo[self.window].relativenumber = false + vim.wo[self.window].signcolumn = 'no' + + self:scroll_to_bottom() +end + +function RunLogger:set_buffer(buffer) + if self:is_opened() then + vim.api.nvim_win_set_buf(self.window, buffer) + else + self:create(buffer) + end + + self:scroll_to_bottom() +end + +function RunLogger:scroll_to_bottom() + local buffer = vim.api.nvim_win_get_buf(self.window) + local line_count = vim.api.nvim_buf_line_count(buffer) + vim.api.nvim_win_set_cursor(self.window, { line_count, 0 }) +end + +---Returns true if the log window is opened +---@return boolean +function RunLogger:is_opened() + if not self.window then + return false + end + + return vim.api.nvim_win_is_valid(self.window) +end + +---Closes the log window if opened +function RunLogger:close() + if self.window and vim.api.nvim_win_is_valid(self.window) then + vim.api.nvim_win_hide(self.window) + end +end + +return RunLogger diff --git a/lua/java-runner/run.lua b/lua/java-runner/run.lua new file mode 100644 index 0000000..41fc87e --- /dev/null +++ b/lua/java-runner/run.lua @@ -0,0 +1,89 @@ +local class = require('java-core.utils.class') +local notify = require('java-core.utils.notify') + +---@class java.Run +---@field name string +---@field main_class string +---@field buffer number +---@field is_running boolean +---@field is_manually_stoped boolean +---@field private term_chan_id number +---@field private job_chan_id number | nil +---@field private is_failure boolean +local Run = class() + +---@param dap_config java-dap.DapLauncherConfig +function Run:_init(dap_config) + self.name = dap_config.name + self.main_class = dap_config.mainClass + self.buffer = vim.api.nvim_create_buf(false, true) + self.term_chan_id = vim.api.nvim_open_term(self.buffer, { + on_input = function(_, _, _, data) + self:send_job(data) + end, + }) +end + +---@param cmd string[] +function Run:start(cmd) + local merged_cmd = table.concat(cmd, ' ') + self.is_running = true + self:send_term(merged_cmd) + + self.job_chan_id = vim.fn.jobstart(merged_cmd, { + pty = true, + on_stdout = function(_, data) + self:send_term(data) + end, + on_exit = function(_, exit_code) + self:on_job_exit(exit_code) + end, + }) +end + +function Run:stop() + if not self.job_chan_id then + return + end + + self.is_manually_stoped = true + vim.fn.jobstop(self.job_chan_id) + vim.fn.jobwait({ self.job_chan_id }, 1000) + self.job_chan_id = nil +end + +---@private +---Send data to execution job channel +---@param data string +function Run:send_job(data) + if self.job_chan_id then + vim.fn.chansend(self.job_chan_id, data) + end +end + +---@private +---Send message to terminal channel +---@param data string +function Run:send_term(data) + vim.fn.chansend(self.term_chan_id, data) +end + +---@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\n', exit_code) + self:send_term(message) + + self.is_running = false + + if exit_code == 0 or self.is_manually_stoped then + self.is_failure = false + self.is_manually_stoped = false + else + self.is_failure = true + notify.error(string.format('%s %s', self.name, message)) + end +end + +return Run diff --git a/lua/java-runner/runner.lua b/lua/java-runner/runner.lua new file mode 100644 index 0000000..bce939e --- /dev/null +++ b/lua/java-runner/runner.lua @@ -0,0 +1,143 @@ +local ui = require('java.ui.utils') +local class = require('java-core.utils.class') +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.setup') + +---@class java.Runner +---@field runs table +---@field logger java.RunLogger +local Runner = class() + +function Runner:_init() + self.runs = {} + self.curr_run = nil + self.logger = RunLogger() +end + +---Starts a new run +---@param args string +function Runner:start_run(args) + local cmd, dap_config = self:select_dap_config(args) + + if not cmd or not dap_config then + return + end + + local run = self.runs[dap_config.mainClass] + + -- get the default run if exist or create new run + if run then + if run.is_running then + run:stop() + end + else + run = Run(dap_config, cmd) + self.runs[dap_config.mainClass] = run + end + + self.curr_run = run + self.logger:set_buffer(run.buffer) + + run:start(cmd) +end + +---Stops the user selected run +function Runner:stop_run() + local run = self:select_run() + + if not run then + return + end + + run:stop() +end + +function Runner:toggle_open_log() + if self.logger:is_opened() then + self.logger:close() + else + if self.curr_run then + self.logger:create(self.curr_run.buffer) + end + end +end + +---Switches the log to selected run +function Runner:switch_log() + local selected_run = self:select_run() + + if not selected_run then + return + end + + self.curr_run = selected_run + self.logger:set_buffer(selected_run.buffer) +end + +---Prompt the user to select an active run and returns the selected run +---@private +---@return java.Run | nil +function Runner:select_run() + local active_main_classes = {} ---@type string[] + + for _, run in pairs(self.runs) do + table.insert(active_main_classes, run.main_class) + end + + local selected_main = ui.select('Select main class', active_main_classes) + + if not selected_main then + return + end + + return self.runs[selected_main] +end + +---Returns the dap config for user selected main +---@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(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) + + if not selected_dap_config then + return nil, nil + end + + local enriched_config = dap:enrich_config(selected_dap_config) + + local class_paths = table.concat(enriched_config.classPaths, ':') + local main_class = enriched_config.mainClass + local java_exec = enriched_config.javaExec + + local active_profile = profile_config.get_active_profile(enriched_config.name) + + local vm_args = '' + local prog_args = args + + if active_profile then + prog_args = (active_profile.prog_args or '') .. ' ' .. (args or '') + vm_args = active_profile.vm_args or '' + end + + local cmd = { + java_exec, + vm_args, + '-cp', + class_paths, + main_class, + prog_args, + } + + return cmd, selected_dap_config +end + +return Runner 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..71103f6 --- /dev/null +++ b/lua/java-test/api.lua @@ -0,0 +1,194 @@ +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) + log.debug('finding test methods for uri: ' .. 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 + + log.debug('found ' .. #methods .. ' test methods') + + 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) + log.debug('running test class from buffer: ' .. buffer) + + local tests = self:get_test_class_by_buffer(buffer) + + if #tests < 1 then + notify.warn('No tests found in the current buffer') + return + end + + log.debug('found ' .. #tests .. ' test classes') + + 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) + 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', 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', + }) + + dap_launcher_config = vim.tbl_deep_extend('force', dap_launcher_config, config or {}) + + log.debug('launching tests with config', dap_launcher_config) + + 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 + +---Run all tests in the workspace +---@param report java-test.JUnitTestReport +---@param config java-dap.DapLauncherConfigOverridable +function M:execute_all_tests(report, config) + log.debug('running all tests') + + local projects = self.test_client:find_java_projects() + + if #projects < 1 then + notify.warn('No Java projects found') + return + end + + -- Discover test classes from all projects + local all_tests = {} + for _, project in ipairs(projects) do + local packages = self.test_client:find_test_packages_and_types(project.jdtHandler) + for _, pkg in ipairs(packages or {}) do + -- Package children are the test classes + for _, test_class in ipairs(pkg.children or {}) do + table.insert(all_tests, test_class) + end + end + end + + if #all_tests < 1 then + notify.warn('No tests found in workspace') + return + end + + log.debug('found ' .. #all_tests .. ' test classes') + self:run_test(all_tests, report, config) +end + +return M diff --git a/lua/java-test/init.lua b/lua/java-test/init.lua new file mode 100644 index 0000000..2c525cc --- /dev/null +++ b/lua/java-test/init.lua @@ -0,0 +1,115 @@ +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.run_all_tests() + log.info('run all tests') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + return test_api:execute_all_tests(M.get_report(), { noDebug = true }) + end) + .catch(get_error_handler('failed to run all tests')) + .run() +end + +function M.debug_all_tests() + log.info('debug all tests') + + return runner(function() + local test_api = JavaTestApi:new({ + client = lsp_utils.get_jdtls(), + runner = DapRunner(), + }) + return test_api:execute_all_tests(M.get_report(), {}) + end) + .catch(get_error_handler('failed to debug all tests')) + .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..4ad687e --- /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.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.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..5e11174 --- /dev/null +++ b/lua/java-test/results/message-id.lua @@ -0,0 +1,28 @@ +---@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', +} + +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..3a0d1e2 --- /dev/null +++ b/lua/java-test/results/result-parser.lua @@ -0,0 +1,215 @@ +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', + [MessageId.TestError] = 'parse_test_failed', +} + +---@private +TestParser.skip_prefixes = { + '@Ignore:', + '@AssumptionFailure:', +} + +---@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 + + for _, prefix in ipairs(TestParser.skip_prefixes) do + if string.match(data[2], '^' .. prefix) then + node.result.status = TestStatus.Skipped + end + end +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 = 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() + + 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..cff718e --- /dev/null +++ b/lua/java-test/results/result-status.lua @@ -0,0 +1,12 @@ +---@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' }, +} + +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..85ebb80 --- /dev/null +++ b/lua/java-test/ui/floating-report-viewer.lua @@ -0,0 +1,95 @@ +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 + 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(indentation).append(result.result.trace, indentation) + 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, '', '') + + FloatingReportViewer.show_in_window(vim.split(res, '\n')) +end + +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 + + 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 6925e81..59332f3 100644 --- a/lua/java.lua +++ b/lua/java.lua @@ -1,76 +1,98 @@ -local decomple_watch = require('java.startup.decompile-watcher') -local mason_dep = require('java.startup.mason-dep') -local nvim_dep = require('java.startup.nvim-dep') -local setup_wrap = require('java.startup.lspconfig-setup-wrap') - -local test = require('java.api.test') -local dap = require('java.api.dap') -local runner = require('java.api.runner') +local runner_api = require('java.api.runner') +local settings_api = require('java.api.settings') local profile_ui = require('java.ui.profile') -local refactor = require('java.api.refactor') - -local global_config = require('java.config') local M = {} +---@param custom_config java.PartialConfig | nil function M.setup(custom_config) - local config = - vim.tbl_deep_extend('force', global_config, custom_config or {}) - vim.g.nvim_java_config = config + local default_conf = require('java.config') + local test_api = require('java-test') - nvim_dep.check() + local config = vim.tbl_deep_extend('force', default_conf, custom_config or {}) + + vim.g.nvim_java_config = config - local is_installing = mason_dep.install(config) + ---------------------------------------------------------------------- + -- checks -- + ---------------------------------------------------------------------- + require('java.checks').run(config) - if not is_installing then - setup_wrap.setup(config) - decomple_watch.setup() - dap.setup_dap_on_lsp_attach() + ---------------------------------------------------------------------- + -- logger setup -- + ---------------------------------------------------------------------- + if config.log then + require('java-core.utils.log2').setup(config.log --[[@as java-core.PartialLog2Config]]) end -end ----------------------------------------------------------------------- --- DAP APIs -- ----------------------------------------------------------------------- -M.dap = {} -M.dap.config_dap = dap.config_dap + ---------------------------------------------------------------------- + -- package installation -- + ---------------------------------------------------------------------- + local Manager = require('pkgm.manager') + local pkgm = Manager() ----------------------------------------------------------------------- --- Test APIs -- ----------------------------------------------------------------------- -M.test = {} -M.test.run_current_class = test.run_current_class -M.test.debug_current_class = test.debug_current_class + pkgm:install('jdtls', config.jdtls.version) -M.test.run_current_method = test.run_current_method -M.test.debug_current_method = test.debug_current_method + if config.java_test.enable then + ---------------------------------------------------------------------- + -- test -- + ---------------------------------------------------------------------- + pkgm:install('java-test', config.java_test.version) -M.test.view_last_report = test.view_last_report + M.test = { + run_current_class = test_api.run_current_class, + debug_current_class = test_api.debug_current_class, ----------------------------------------------------------------------- --- Manipulate -- ----------------------------------------------------------------------- + run_current_method = test_api.run_current_method, + debug_current_method = test_api.debug_current_method, -M.manipulate = {} --- M.manipulate.organize_imports = {} + view_last_report = test_api.view_last_report, + } + end ----------------------------------------------------------------------- --- Refactor -- ----------------------------------------------------------------------- -M.refactor = {} -M.refactor.extract_variable = refactor.extract_variable + 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 + + if config.spring_boot_tools.enable then + pkgm:install('spring-boot-tools', config.spring_boot_tools.version) + end + + if config.lombok.enable then + pkgm:install('lombok', config.lombok.version) + end + + if config.jdk.auto_install then + pkgm:install('openjdk', config.jdk.version) + end + + ---------------------------------------------------------------------- + -- init -- + ---------------------------------------------------------------------- + require('java.startup.lsp_setup').setup(config) + require('java.startup.decompile-watcher').setup() + require('java-refactor').setup() +end ---------------------------------------------------------------------- -- Runner APIs -- ---------------------------------------------------------------------- M.runner = {} -M.runner.run_app = runner.run_app - M.runner.built_in = {} -M.runner.built_in.run_app = runner.built_in.run_app -M.runner.built_in.toggle_logs = runner.built_in.toggle_logs -M.runner.built_in.stop_app = runner.built_in.stop_app -M.runner.built_in.switch_app = runner.built_in.switch_app +M.runner.built_in.run_app = runner_api.built_in.run_app +M.runner.built_in.toggle_logs = runner_api.built_in.toggle_logs +M.runner.built_in.stop_app = runner_api.built_in.stop_app +M.runner.built_in.switch_app = runner_api.built_in.switch_app ---------------------------------------------------------------------- -- Profile UI -- @@ -78,8 +100,10 @@ M.runner.built_in.switch_app = runner.built_in.switch_app M.profile = {} M.profile.ui = profile_ui.ui -function M.__run() - test.debug_current_method() -end +---------------------------------------------------------------------- +-- Settings -- +---------------------------------------------------------------------- +M.settings = {} +M.settings.change_runtime = settings_api.change_runtime return M 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/refactor.lua b/lua/java/api/refactor.lua deleted file mode 100644 index ec43fb1..0000000 --- a/lua/java/api/refactor.lua +++ /dev/null @@ -1,18 +0,0 @@ -local jdtls = require('java.utils.jdtls2') -local get_error_handler = require('java.handlers.error') - -local async = require('java-core.utils.async').sync - -local M = {} - -function M.extract_variable() - return async(function() - local RefactorCommands = require('java-refactor.refactor-commands') - local refactor_commands = RefactorCommands(jdtls()) - refactor_commands:extract_variable() - end) - .catch(get_error_handler('failed to refactor variable')) - .run() -end - -return M diff --git a/lua/java/api/runner.lua b/lua/java/api/runner.lua index 84a909e..ae1d8aa 100644 --- a/lua/java/api/runner.lua +++ b/lua/java/api/runner.lua @@ -1,362 +1,45 @@ -local log = require('java.utils.log') -local async = require('java-core.utils.async').sync -local get_error_handler = require('java.handlers.error') -local jdtls = require('java.utils.jdtls') -local DapSetup = require('java-dap.api.setup') -local ui = require('java.utils.ui') -local profile_config = require('java.api.profile_config') -local class = require('java-core.utils.class') - -local group = vim.api.nvim_create_augroup('logger', { clear = true }) - ---- @class RunnerApi ---- @field client LspClient ---- @field private dap java.DapSetup -local RunnerApi = class() - -function RunnerApi:_init(args) - self.client = args.client - self.dap = DapSetup(args.client) -end - -function RunnerApi:get_config() - local configs = self.dap:get_dap_config() - return ui.select_from_dap_configs(configs) -end - ---- @param callback fun(cmd, config) ---- @param args string -function RunnerApi:run_app(callback, args) - local config = self:get_config() - if not config then - return - end - - config.request = 'launch' - local enrich_config = self.dap:enrich_config(config) - local class_paths = table.concat(enrich_config.classPaths, ':') - local main_class = enrich_config.mainClass - local java_exec = enrich_config.javaExec - - local active_profile = - profile_config.get_active_profile(enrich_config.mainClass) - - local vm_args = '' - local prog_args = '' - if active_profile then - prog_args = (active_profile.prog_args or '') .. ' ' .. (args or '') - vm_args = active_profile.vm_args or '' - end - - local cmd = { - java_exec, - vm_args, - '-cp', - class_paths, - main_class, - prog_args, - } - log.debug('run app cmd: ', cmd) - callback(cmd, config) -end - ---- @class RunningApp ---- @field win number ---- @field bufnr number ---- @field job_id number ---- @field chan number ---- @field dap_config table ---- @field is_open boolean ---- @field running_status string -local RunningApp = class() - ---- @param dap_config table -function RunningApp:_init(dap_config) - self.is_open = false - self.dap_config = dap_config - self.bufnr = vim.api.nvim_create_buf(false, true) -end - ---- @class BuiltInMainRunner ---- @field running_apps table ---- @field current_app RunningApp -local BuiltInMainRunner = class() - -function BuiltInMainRunner:_init() - self.running_apps = {} - self.current_app = nil -end - ---- @param running_app RunningApp -function BuiltInMainRunner.set_up_buffer_autocmd(running_app) - vim.api.nvim_create_autocmd({ 'BufHidden' }, { - group = group, - buffer = running_app.bufnr, - callback = function(_) - running_app.is_open = false - end, - }) -end - ---- @param running_app RunningApp -function BuiltInMainRunner.stop(running_app) - if running_app.job_id ~= nil then - vim.fn.jobstop(running_app.job_id) - vim.fn.jobwait({ running_app.job_id }, 1000) - running_app.job_id = nil - end -end - ---- @param running_app RunningApp -function BuiltInMainRunner.set_up_buffer(running_app) - vim.cmd('sp | winc J | res 15 | buffer ' .. running_app.bufnr) - running_app.win = vim.api.nvim_get_current_win() - - vim.wo[running_app.win].number = false - vim.wo[running_app.win].relativenumber = false - vim.wo[running_app.win].signcolumn = 'no' - - BuiltInMainRunner.set_up_buffer_autocmd(running_app) - running_app.is_open = true -end - ---- @param exit_code number ---- @param running_app RunningApp -function BuiltInMainRunner.on_exit(exit_code, running_app) - local exit_message = 'Process finished with exit code ' .. exit_code - vim.fn.chansend(running_app.chan, '\n' .. exit_message .. '\n') - local current_buf = vim.api.nvim_get_current_buf() - if current_buf == running_app.bufnr then - vim.cmd('stopinsert') - end - vim.notify(running_app.dap_config.name .. ' ' .. exit_message) - running_app.running_status = exit_message -end - ---- @param running_app RunningApp -function BuiltInMainRunner.scroll_down(running_app) - local last_line = vim.api.nvim_buf_line_count(running_app.bufnr) - vim.api.nvim_win_set_cursor(running_app.win, { last_line, 0 }) -end - ---- @param running_app RunningApp -function BuiltInMainRunner.hide_logs(running_app) - if not running_app or not running_app.is_open then - return - end - if running_app.bufnr then - vim.api.nvim_buf_call(running_app.bufnr, function() - vim.cmd('hide') - end) - end -end - ---- @param running_app RunningApp -function BuiltInMainRunner.toggle_logs(running_app) - if running_app and not running_app.is_open then - vim.api.nvim_buf_call(running_app.bufnr, function() - BuiltInMainRunner.set_up_buffer(running_app) - BuiltInMainRunner.scroll_down(running_app) - end) - else - BuiltInMainRunner.hide_logs(running_app) - end -end - -function BuiltInMainRunner:toggle_current_app_logs() - local running_app = self.current_app - if not running_app then - return - end - BuiltInMainRunner.toggle_logs(running_app) -end - ---- @param data string[] ---- @param running_app RunningApp -function BuiltInMainRunner.on_stdout(data, running_app) - vim.fn.chansend(running_app.chan, data) - if not running_app.is_open then - return - end - vim.api.nvim_buf_call(running_app.bufnr, function() - local current_buf = vim.api.nvim_get_current_buf() - local mode = vim.api.nvim_get_mode().mode - if current_buf ~= running_app.bufnr or mode ~= 'i' then - BuiltInMainRunner.scroll_down(running_app) - end - end) -end - ---- @param cmd string[] ---- @param config table -function BuiltInMainRunner:run_app(cmd, config) - local selected_app = self:select_app_with_dap_config(config) - if self.current_app then - if self.current_app.job_id == selected_app.job_id then - BuiltInMainRunner.stop(selected_app) - end - end - self:change_current_app(selected_app) - if not selected_app.is_open or not selected_app.chan then - vim.api.nvim_buf_call(selected_app.bufnr, function() - BuiltInMainRunner.set_up_buffer(selected_app) - BuiltInMainRunner.scroll_down(selected_app) - end) - vim.api.nvim_buf_set_name(selected_app.bufnr, selected_app.dap_config.name) - end - - selected_app.chan = vim.api.nvim_open_term(selected_app.bufnr, { - on_input = function(_, _, _, data) - if selected_app.job_id then - vim.fn.chansend(selected_app.job_id, data) - end - end, - }) - - selected_app.running_status = '(running)' - local command = table.concat(cmd, ' ') - vim.fn.chansend(selected_app.chan, command) - selected_app.job_id = vim.fn.jobstart(command, { - pty = true, - on_stdout = function(_, data) - BuiltInMainRunner.on_stdout(data, selected_app) - end, - on_exit = function(_, exit_code) - BuiltInMainRunner.on_exit(exit_code, selected_app) - end, - }) -end - ---- @return RunningApp|nil -function BuiltInMainRunner:select_app_with_ui() - --- @type RunningApp - local app = nil - local count = 0 - for _ in pairs(self.running_apps) do - count = count + 1 - end - if count == 0 then - error('No running apps') - elseif count == 1 then - for _, _app in pairs(self.running_apps) do - app = _app - end - else - local configs = {} - for _, _app in pairs(self.running_apps) do - _app.dap_config.extra_args = _app.running_status - table.insert(configs, _app.dap_config) - end - - local selected = ui.select_from_dap_configs(configs) - if not selected then - return nil - end - app = self.running_apps[selected.mainClass] - end - self:change_current_app(app) - return app -end - ---- @param app RunningApp -function BuiltInMainRunner:change_current_app(app) - if - self.current_app - and self.current_app.dap_config.name ~= app.dap_config.name - then - BuiltInMainRunner.hide_logs(self.current_app) - end - self.current_app = app -end - ---- @param config table|nil ---- @return RunningApp -function BuiltInMainRunner:select_app_with_dap_config(config) - --- @type RunningApp - if not config then - error('No config') - end - - if self.running_apps and self.running_apps[config.mainClass] then - return self.running_apps[config.mainClass] - end - local running_app = RunningApp(config) - self.running_apps[running_app.dap_config.mainClass] = running_app - return running_app -end - -function BuiltInMainRunner:switch_app() - local selected_app = self:select_app_with_ui() - if not selected_app then - return - end - if not selected_app.is_open then - BuiltInMainRunner.toggle_logs(selected_app) - end -end - -function BuiltInMainRunner:stop_app() - local selected_app = self:select_app_with_ui() - if not selected_app then - return - end - BuiltInMainRunner.stop(selected_app) - BuiltInMainRunner.toggle_logs(selected_app) -end +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 = {}, - --- @type BuiltInMainRunner - runner = BuiltInMainRunner(), -} ---- @param callback fun(cmd, config) ---- @param args string -function M.run_app(callback, args) - return async(function() - return RunnerApi(jdtls()):run_app(callback, args) - end) - .catch(get_error_handler('failed to run app')) - .run() -end + ---@type java.Runner + runner = Runner(), +} --- @param opts {} function M.built_in.run_app(opts) - async(function() - M.run_app(function(cmd, config) - M.runner:run_app(cmd, config) - end, opts.args) + runner(function() + M.runner:start_run(opts.args) end) - .catch(get_error_handler('failed to run app')) + .catch(get_error_handler('Failed to run app')) .run() end function M.built_in.toggle_logs() - async(function() - M.runner:toggle_current_app_logs() + runner(function() + M.runner:toggle_open_log() end) - .catch(get_error_handler('failed to toggle logs')) + .catch(get_error_handler('Failed to run app')) .run() end function M.built_in.switch_app() - async(function() - M.runner:switch_app() + runner(function() + M.runner:switch_log() end) - .catch(get_error_handler('failed to switch app')) + .catch(get_error_handler('Failed to switch run')) .run() end function M.built_in.stop_app() - async(function() - M.runner:stop_app() + runner(function() + M.runner:stop_run() end) - .catch(get_error_handler('failed to stop app')) + .catch(get_error_handler('Failed to stop run')) .run() end -M.RunnerApi = RunnerApi -M.BuiltInMainRunner = BuiltInMainRunner -M.RunningApp = RunningApp - return M diff --git a/lua/java/api/settings.lua b/lua/java/api/settings.lua new file mode 100644 index 0000000..0349d90 --- /dev/null +++ b/lua/java/api/settings.lua @@ -0,0 +1,47 @@ +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.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 = lsp_utils.get_jdtls() + + ---@type RuntimeOption[] + local runtimes = conf_utils.get_property_from_conf(client.config, 'settings.java.configuration.runtimes', {}) + + if #runtimes < 1 then + notify.error( + 'No configured runtimes available' + .. '\nRefer following link for instructions define available runtimes' + .. '\nhttps://github.com/nvim-java/nvim-java?tab=readme-ov-file#clamp-how-to-use-jdk-xx-version' + ) + return + end + + local jdtls = JdtlsClient(client) + + 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 + if sel_runtime.path == runtime.path then + runtime.default = true + else + runtime.default = nil + end + end + + jdtls:workspace_did_change_configuration(client.config.settings) + end) + .catch(get_error_handler('Changing runtime failed')) + .run() +end + +return M 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/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..f5b6b49 --- /dev/null +++ b/lua/java/checks/nvim-version.lua @@ -0,0 +1,19 @@ +local M = {} + +---Run nvim version check +---@param config java.Config +function M:run(config) + if not config.checks.nvim_version then + return + end + + if not vim.version.ge(vim.version(), '0.11.5') 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. + ]]) + end +end + +return M diff --git a/lua/java/config.lua b/lua/java/config.lua index e013d1e..8b65edc 100644 --- a/lua/java/config.lua +++ b/lua/java/config.lua @@ -1,41 +1,89 @@ +local JDTLS_VERSION = '1.54.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', + }, + ['1.54.0'] = { + lombok = '1.18.42', + java_test = '0.43.2', + java_debug_adapter = '0.58.3', + spring_boot_tools = '1.55.1', + jdk = '25', + }, +} + +local V = jdtls_version_map[JDTLS_VERSION] + ---@class java.Config ----@field root_markers string[] ----@field java_test { enable: boolean } ----@field java_debug_adapter { enable: boolean } ----@field jdk { auto_install: boolean } ----@field notifications { dap: 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 } +---@field java_debug_adapter { enable: boolean, version: string } +---@field spring_boot_tools { enable: boolean, version: string } +---@field jdk { auto_install: boolean, version: string } +---@field log java-core.Log2Config + +---@class java.PartialConfig +---@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 } +---@field java_debug_adapter? { enable?: boolean, version?: string } +---@field spring_boot_tools? { enable?: boolean, version?: string } +---@field jdk? { auto_install?: boolean, version?: string } +---@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, + nvim_jdtls_conflict = true, + }, + + jdtls = { + version = JDTLS_VERSION, + }, + + lombok = { + enable = true, + version = V.lombok, }, -- load java test plugins java_test = { enable = true, + version = V.java_test, }, -- load java debugger plugins java_debug_adapter = { enable = true, + version = V.java_debug_adapter, + }, + + spring_boot_tools = { + enable = true, + version = V.spring_boot_tools, }, jdk = { - -- install jdk using mason.nvim auto_install = true, + version = V.jdk, }, - notifications = { - -- enable 'Configuring DAP' & 'DAP configured' messages on start up - dap = true, + 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 42d78f7..0000000 --- a/lua/java/dap/init.lua +++ /dev/null @@ -1,111 +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 = dap_config -end - -return M diff --git a/lua/java/startup/decompile-watcher.lua b/lua/java/startup/decompile-watcher.lua index 078cc44..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.jdtls') -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,12 +21,11 @@ function M.setup() callback = function(opts) local done = false - async(function() - local client_obj = jdtls() + runner(function() + local client = lsp_utils.get_jdtls() local buffer = opts.buf - local text = JavaCoreJdtlsClient:new(client_obj) - :java_decompile(opts.file) + local text = JavaCoreJdtlsClient(client):java_decompile(opts.file) local lines = vim.split(text, '\n') @@ -38,13 +37,13 @@ function M.setup() vim.bo[buffer].filetype = 'java' vim.bo[buffer].modifiable = false - if not vim.lsp.buf_is_attached(buffer, client_obj.client.id) then - vim.lsp.buf_attach_client(buffer, client_obj.client.id) + if not vim.lsp.buf_is_attached(buffer, client.id) then + vim.lsp.buf_attach_client(buffer, client.id) end 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/lsp_setup.lua b/lua/java/startup/lsp_setup.lua new file mode 100644 index 0000000..49e5cbd --- /dev/null +++ b/lua/java/startup/lsp_setup.lua @@ -0,0 +1,42 @@ +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({ + config = config, + plugins = jdtls_plugins, + }) + + 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 f179d67..0000000 --- a/lua/java/startup/lspconfig-setup-wrap.lua +++ /dev/null @@ -1,36 +0,0 @@ -local lspconfig = require('lspconfig') -local log = require('java.utils.log') - -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) - 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 - - local default_config = server.get_config({ - root_markers = config.root_markers, - jdtls_plugins = jdtls_plugins, - use_mason_jdk = config.jdk.auto_install, - }) - - 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 8621732..0000000 --- a/lua/java/startup/mason-dep.lua +++ /dev/null @@ -1,60 +0,0 @@ -local log = require('java.utils.log') -local mason_ui = require('mason.ui') -local mason_util = require('java.utils.mason') -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 M = {} - ----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 dependecies are installed') - end) - - mason_util.refresh_registry() - mason_util.install_pkgs(packages) -end - -function M.get_pkg_list(config) - local dependecies = { - { name = 'jdtls', version = 'v1.31.0' }, - { name = 'lombok-nightly', version = 'nightly' }, - { name = 'java-test', version = '0.40.1' }, - { name = 'java-debug-adapter', version = '0.55.0' }, - } - - if config.jdk.auto_install then - table.insert(dependecies, { name = 'openjdk-17', version = '17.0.2' }) - end - - return dependecies -end - -return M diff --git a/lua/java/startup/nvim-dep.lua b/lua/java/startup/nvim-dep.lua deleted file mode 100644 index e304190..0000000 --- a/lua/java/startup/nvim-dep.lua +++ /dev/null @@ -1,61 +0,0 @@ -local notify = require('java-core.utils.notify') -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.check() - log.info('check neovim plugin dependencies') - M.neovim_plugin_check() -end - ----@private -function M.neovim_plugin_check() - for _, pkg in ipairs(pkgs) do - local ok, _ = pcall(require, pkg.name) - - if not ok then - if pkg.warn then - log.warn(pkg.warn) - notify.warn(pkg.warn) - else - log.error(pkg.err) - error(pkg.err) - end - end - end -end - -return M 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 023a3a9..83876ff 100644 --- a/lua/java/ui/profile.lua +++ b/lua/java/ui/profile.lua @@ -1,15 +1,15 @@ 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 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 +147,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 .. ']', @@ -188,7 +183,6 @@ function ProfileUI:_get_and_fill_popup( win_options = self.win_options, }) - log.error(vim.inspect(popup.border)) -- fill the popup with the config value -- if target_profile is nil, it's a new profile if target_profile then @@ -203,29 +197,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 +256,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 +282,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,29 +315,39 @@ 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 dap_config = DapSetup(jdtls().client):get_dap_config() - local selected_config = ui.select_from_dap_configs(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) + if not selected_config then return end + M.profile_ui = ProfileUI(selected_config.name) return M.profile_ui:openMenu() end) diff --git a/lua/java/ui/utils.lua b/lua/java/ui/utils.lua new file mode 100644 index 0000000..d04a243 --- /dev/null +++ b/lua/java/ui/utils.lua @@ -0,0 +1,110 @@ +local wait = require('async.waits.wait') +local List = require('java-core.utils.list') + +local M = {} + +---Async vim.ui.select function +---@generic T +---@param prompt string +---@param values T[] +---@param format_item? fun(item: T): string +---@param opts? { return_one: boolean } +---@return T | nil +function M.select(prompt, values, format_item, opts) + opts = opts or { prompt_single = false } + + return wait(function(callback) + if not opts.prompt_single and #values == 1 then + callback(values[1]) + return + end + + vim.ui.select(values, { + prompt = prompt, + format_item = format_item, + }, callback) + end) +end + +--Sync vim.ui.select function +---@generic T +---@param prompt string +---@param values T[] +---@param format_item? fun(item: T, index: number): string +---@param opts? { return_one: boolean } +---@return T | nil +function M.select_sync(prompt, values, format_item, opts) + opts = opts or { prompt_single = false } + + if not opts.prompt_single and #values == 1 then + return values[1] + end + + local labels = { prompt } + for index, value in ipairs(values) do + table.insert(labels, format_item and format_item(value, index) or value) + end + + local selected_index = vim.fn.inputlist(labels) + + return values[selected_index] +end + +---Async vim.ui.select function +---@generic T +---@param prompt string +---@param values T[] +---@param format_item? fun(item: T): string +---@return T[] | nil +function M.multi_select(prompt, values, format_item) + return wait(function(callback) + local wrapped_items = List:new(values):map(function(item, index) + return { + index = index, + is_selected = false, + value = item, + } + end) + + local open_select + + open_select = function() + vim.ui.select(wrapped_items, { + 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) + end, + }, function(selected) + if not selected then + local selected_items = wrapped_items + :filter(function(item) + return item.is_selected + end) + :map(function(item) + return item.value + end) + + callback(#selected_items > 0 and selected_items or nil) + return + end + + wrapped_items[selected.index].is_selected = not wrapped_items[selected.index].is_selected + + open_select() + end) + end + + open_select() + end) +end + +function M.input(prompt) + return wait(function(callback) + vim.ui.input({ + prompt = prompt, + }, callback) + end) +end + +return M diff --git a/lua/java/utils/config.lua b/lua/java/utils/config.lua new file mode 100644 index 0000000..eaf9f3b --- /dev/null +++ b/lua/java/utils/config.lua @@ -0,0 +1,17 @@ +local M = {} + +function M.get_property_from_conf(config, path, default) + local node = config + + for key in string.gmatch(path, '([^.]+)') do + if not node[key] then + return default + end + + node = node[key] + end + + return node +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 5458847..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 { client: LspClient } -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 6e239a2..0000000 --- a/lua/java/utils/mason.lua +++ /dev/null @@ -1,80 +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 M = {} - -function M.is_available(package_name, package_version) - local has_pkg = mason_reg.has_package(package_name) - - if not has_pkg then - return false - end - - local has_version = false - - local pkg = mason_reg.get_package(package_name) - pkg:get_installed_version(function(success, version) - if success and version == package_version then - has_version = true - end - end) - - return has_version -end - -function M.is_installed(package_name, package_version) - local pkg = mason_reg.get_package(package_name) - local is_installed = pkg:is_installed() - - if not is_installed then - return false - end - - local installed_version - pkg:get_installed_version(function(ok, version) - if not ok then - return - end - - installed_version = version - 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) - - pkg:install({ - version = dep.version, - force = true, - }) - end - end -end - -return M diff --git a/lua/java/utils/ui.lua b/lua/java/utils/ui.lua deleted file mode 100644 index 214112d..0000000 --- a/lua/java/utils/ui.lua +++ /dev/null @@ -1,52 +0,0 @@ -local async = require('java-core.utils.async') -local await = async.wait -local notify = require('java-core.utils.notify') - -local M = {} - -function M.select(prompt, values) - return await(function(callback) - vim.ui.select(values, { - prompt = prompt, - }, callback) - end) -end - -function M.input(prompt) - return await(function(callback) - vim.ui.input({ - prompt = prompt, - }, callback) - end) -end - ----@param configs table -function M.select_from_dap_configs(configs) - local config_names = {} - local config_lookup = {} - for _, config in ipairs(configs) do - if config.projectName then - local key = config.name - if config.extra_args then - key = key .. ' | ' .. config.extra_args - end - table.insert(config_names, key) - config_lookup[key] = config - end - end - - if #config_names == 0 then - notify.warn('Config not found') - return - end - - if #config_names == 1 then - return config_lookup[config_names[1]] - end - - local selected_config = - M.select('Select the main class (modul -> mainClass)', config_names) - return config_lookup[selected_config] -end - -return M diff --git a/lua/pkgm/downloaders/curl.lua b/lua/pkgm/downloaders/curl.lua new file mode 100644 index 0000000..2009ab8 --- /dev/null +++ b/lua/pkgm/downloaders/curl.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.Curl +---@field url string +---@field dest string +---@field retry_count number +---@field timeout number +local Curl = class() + +---@class java-core.CurlOpts +---@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.CurlOpts +function Curl:_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 curl +---@return string|nil # Path to downloaded file, or nil on failure +---@return string|nil # Error message if failed +function Curl:download() + log.debug('curl downloading:', self.url, 'to', self.dest) + local cmd = string.format( + 'curl --retry %d --connect-timeout %d -o %s %s', + self.retry_count, + self.timeout, + vim.fn.shellescape(self.dest), + vim.fn.shellescape(self.url) + ) + log.debug('curl command:', cmd) + + local result = vim.fn.system(cmd) + local exit_code = vim.v.shell_error + + if exit_code ~= 0 then + log.error('curl failed:', exit_code, result) + return nil, string.format('curl failed (exit %d): %s', exit_code, result) + end + + log.debug('curl download completed:', self.dest) + return self.dest, nil +end + +return Curl diff --git a/lua/pkgm/downloaders/factory.lua b/lua/pkgm/downloaders/factory.lua new file mode 100644 index 0000000..a0163c4 --- /dev/null +++ b/lua/pkgm/downloaders/factory.lua @@ -0,0 +1,47 @@ +local system = require('java-core.utils.system') +local Curl = require('pkgm.downloaders.curl') +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 + + -- Check for curl on all platforms + if vim.fn.executable('curl') == 1 then + log.debug('Using curl downloader') + return Curl(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..f0a5434 --- /dev/null +++ b/lua/pkgm/downloaders/powershell.lua @@ -0,0 +1,72 @@ +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 +---@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) + 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 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..3d88036 --- /dev/null +++ b/lua/pkgm/extractors/tar.lua @@ -0,0 +1,75 @@ +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 + +---@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 +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 %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( + '%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..f97d4ba --- /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', 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', 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..e92a0cb --- /dev/null +++ b/lua/pkgm/specs/init.lua @@ -0,0 +1,108 @@ +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.54.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', + }, + }, + }, + }), + + -- https://download.java.net/java/GA/jdk25.0.1/2fbf10d8c78e40bd87641c434705079d/8/GPL/openjdk-25.0.1_linux-x64_bin.tar.gz + + BaseSpec({ + name = 'openjdk', + version = '25', + urls = { + linux = { + arm = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/latest/jdk-{{version}}_linux-aarch64_bin.tar.gz', + }, + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/latest/jdk-{{version}}_linux-x64_bin.tar.gz', + }, + }, + mac = { + arm = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/latest/jdk-{{version}}_macos-aarch64_bin.tar.gz', + }, + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/latest/jdk-{{version}}_macos-x64_bin.tar.gz', + }, + }, + win = { + x86 = { + ['64bit'] = 'https://download.oracle.com/java/{{version}}/latest/jdk-{{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..b5b2cc5 --- /dev/null +++ b/lua/pkgm/specs/jdtls-spec/version-map.lua @@ -0,0 +1,23 @@ +-- 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', + ['1.54.0'] = '202511261751', +} diff --git a/plugin/java.lua b/plugin/java.lua index 9c28519..5feba3f 100644 --- a/plugin/java.lua +++ b/plugin/java.lua @@ -5,15 +5,53 @@ local function c(cmd, callback, opts) end local cmd_map = { - JavaDapConfig = { java.dap.config_dap }, + JavaSettingsChangeRuntime = { java.settings.change_runtime }, - JavaTestRunCurrentClass = { java.test.run_current_class }, - JavaTestDebugCurrentClass = { java.test.debug_current_class }, + JavaDapConfig = { + function() + require('java-dap').config_dap() + end, + }, - JavaTestRunCurrentMethod = { java.test.run_current_method }, - JavaTestDebugCurrentMethod = { java.test.debug_current_method }, + JavaTestRunCurrentClass = { + function() + require('java-test').run_current_class() + end, + }, + JavaTestDebugCurrentClass = { + function() + require('java-test').debug_current_class() + end, + }, - JavaTestViewLastReport = { java.test.view_last_report }, + JavaTestRunCurrentMethod = { + function() + require('java-test').run_current_method() + end, + }, + JavaTestDebugCurrentMethod = { + function() + require('java-test').debug_current_method() + end, + }, + + JavaTestRunAllTests = { + function() + require('java-test').run_all_tests() + end, + }, + + JavaTestDebugAllTests = { + function() + require('java-test').debug_all_tests() + 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 }, @@ -21,11 +59,6 @@ local cmd_map = { JavaRunnerSwitchLogs = { java.runner.built_in.switch_app }, JavaProfile = { java.profile.ui }, - - JavaRefactorExtractVariable = { - java.refactor.extract_variable, - { range = 2 }, - }, } for cmd, details in pairs(cmd_map) do diff --git a/tests/constants/capabilities.lua b/tests/constants/capabilities.lua new file mode 100644 index 0000000..fa4572b --- /dev/null +++ b/tests/constants/capabilities.lua @@ -0,0 +1,81 @@ +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.getTroubleshootingInfo', + '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.jacoco.getCoverageDetail', + '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/java/api/profile_config_spec.lua b/tests/java/api/profile_config_spec.lua deleted file mode 100644 index e926651..0000000 --- a/tests/java/api/profile_config_spec.lua +++ /dev/null @@ -1,422 +0,0 @@ -local mock = require('luassert.mock') -local Profile = require('java.api.profile_config').Profile -local spy = require('luassert.spy') - -describe('java.api.profile_config', function() - local project_path = 'project_path' - local mock_fn = {} - local mock_io = {} - local file_mock = { - read = function(_, readmode) - assert(readmode == '*a') - return '{ "test": "test" }' - end, - close = function() end, - write = function() end, - } - - local json_encode = vim.fn.json_encode - local json_decode = vim.fn.json_decode - - before_each(function() - package.loaded['java.api.profile_config'] = nil - mock_io = mock(io, true) - mock_fn = mock(vim.fn, true) - - mock_fn.json_decode = function(data) - return json_decode(data) - end - - mock_fn.json_encode = function(data) - return json_encode(data) - end - - mock_fn.getcwd = function() - return project_path - end - - mock_fn.stdpath = function() - return 'config_path' - end - - mock_fn.expand = function() - return project_path - end - end) - - after_each(function() - mock.revert(mock_fn) - mock.revert(mock_io) - end) - - describe('read_config', function() - it('when no config file', function() - mock_io.open = function() - return nil - end - local profile_config = require('java.api.profile_config') - profile_config.setup() - assert.same(profile_config.get_all_profiles('module1 -> main_class'), {}) - end) - - it('when config cannot be decoded', function() - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{' - end - mock_io.open = function() - return file_mock - end - - local profile_config = require('java.api.profile_config') - profile_config.setup(project_path) - assert.same(profile_config.get_all_profiles('module1 -> main_class'), {}) - end) - - it('successfully', function() - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": \ - { \ - "module1 -> main_class": {\ - "profile_name1":{ \ - "vm_args": "vm_args",\ - "prog_args": "prog_args",\ - "name": "profile_name1",\ - "is_active": true }\ - } \ - }\ - }' - end - - mock_io.open = function(config_path, mode) - assert(config_path == 'config_path/nvim-java-profiles.json') - assert(mode == 'r') - return file_mock - end - - local profile_config = require('java.api.profile_config') - profile_config.setup(project_path) - assert.same(profile_config.get_all_profiles('module1 -> main_class'), { - profile_name1 = Profile('vm_args', 'prog_args', 'profile_name1', true), - }) - end) - end) - - describe('add_or_update_profile', function() - it('when updating profile', function() - local input_profile = - Profile('vm_args_updated', 'prog_args_updated', 'name', true) - - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": {\ - "module1 -> main_class": {\ - "profile_name1":{\ - "vm_args": "vm_args",\ - "prog_args": "prog_args",\ - "name": "name",\ - "is_active": true }\ - }\ - }\ - }' - end - - local spy_close = spy.on(file_mock, 'close') - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - profile_config.add_or_update_profile( - input_profile, - 'name', - 'module1 -> main_class' - ) - --- verify - assert.same( - { name = input_profile }, - profile_config.get_all_profiles('module1 -> main_class') - ) - assert.spy(spy_close).was_called(3) -- init, full_config, save - end) - end) - - it('when add new profile (set activate automatically)', function() - local input_profile = Profile('vm_args2', 'prog_args2', 'name2') - - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": { \ - "module1 -> main_class": [\ - { \ - "vm_args": "vm_args1",\ - "prog_args": "prog_args1",\ - "name": "name1",\ - "is_active": true }\ - ]\ - }\ - }' - end - - mock_fn.json_encode = function(data) - local expected1_id = 1 - local expected2_id = 2 - - if data.project_path['module1 -> main_class'][1].name == 'name2' then - expected1_id = 2 - expected2_id = 1 - end - - assert.same(data.project_path['module1 -> main_class'][expected1_id], { - vm_args = 'vm_args1', - prog_args = 'prog_args1', - name = 'name1', - is_active = false, - }) - assert.same(data.project_path['module1 -> main_class'][expected2_id], { - vm_args = 'vm_args2', - prog_args = 'prog_args2', - name = 'name2', - is_active = true, - }) - return json_encode(data) - end - - local spy_close = spy.on(file_mock, 'close') - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - profile_config.add_or_update_profile( - input_profile, - nil, - 'module1 -> main_class' - ) - - local expected_app_profiles_output = { - name1 = Profile('vm_args1', 'prog_args1', 'name1', false), - name2 = Profile('vm_args2', 'prog_args2', 'name2', true), - } - assert.same( - profile_config.get_all_profiles('module1 -> main_class'), - expected_app_profiles_output - ) - assert.spy(spy_close).was_called(3) -- init, full_config, save - end) - - it('when profile already exists', function() - -- setup - local input_profile = Profile('vm_args1', 'prog_args1', 'name1') - - -- mocks/verify mocks calls with expected values - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": { \ - "module1 -> main_class": {\ - "profile_name1":{\ - "vm_args": "vm_args1",\ - "prog_args": "prog_args1",\ - "name": "name1",\ - "is_active": true }\ - }\ - }\ - }' - end - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - assert.has.error(function() - profile_config.add_or_update_profile( - input_profile, - nil, - 'module1 -> main_class' - ) - end, "Profile with name 'name1' already exists") - end) - - it('when profile name is required', function() - -- setup - local input_profile = Profile('vm_args', 'prog_args', nil) - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - assert.has.error(function() - profile_config.add_or_update_profile( - input_profile, - nil, - 'module1 -> main_class' - ) - end, 'Profile name is required') - end) - - it('set_active_profile', function() - -- mocks/verify mocks calls with expected values - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": \ - { \ - "module1 -> main_class": [{ \ - "vm_args": "vm_args",\ - "prog_args": "prog_args",\ - "name": "name1",\ - "is_active": true \ - },\ - { \ - "vm_args": "vm_args",\ - "prog_args": "prog_args",\ - "name": "name2",\ - "is_active": false \ - }] \ - }\ - }' - end - - local spy_close = spy.on(file_mock, 'close') - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - profile_config.set_active_profile('name2', 'module1 -> main_class') - - --- verify - assert.same( - profile_config.get_active_profile('module1 -> main_class'), - Profile('vm_args', 'prog_args', 'name2', true) - ) - assert.spy(spy_close).was_called(3) -- init, full_config, save - end) - - it('get_profile_by_name', function() - -- setup - - -- mocks/verify mocks calls with expected values - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": \ - { \ - "module1 -> main_class": [{ \ - "vm_args": "vm_args1",\ - "prog_args": "prog_args1",\ - "name": "name1",\ - "is_active": true \ - },\ - { \ - "vm_args": "vm_args2",\ - "prog_args": "prog_args2",\ - "name": "name2",\ - "is_active": false \ - }] \ - }\ - }' - end - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - local profile = profile_config.get_profile('name1', 'module1 -> main_class') - --- verify - assert.same(profile, Profile('vm_args1', 'prog_args1', 'name1', true)) - end) - - describe('get_active_profile', function() - -- mocks/verify mocks calls with expected values - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": \ - { \ - "module1 -> main_class": [{ \ - "vm_args": "vm_args1",\ - "prog_args": "prog_args1",\ - "name": "name1",\ - "is_active": true \ - },\ - { \ - "vm_args": "vm_args2",\ - "prog_args": "prog_args2",\ - "name": "name2",\ - "is_active": false \ - }] \ - }\ - }' - end - - it('succesfully', function() - -- setup - mock_io.open = function() - return file_mock - end - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - local profile = profile_config.get_active_profile('module1 -> main_class') - - --- verify - assert.same(profile, Profile('vm_args1', 'prog_args1', 'name1', true)) - end) - - it('when number_of_profiles == 0', function() - -- setup - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": { "module1 -> main_class": [] } }' - end - - mock_io.open = function() - return file_mock - end - - -- call the function - local profile_config = require('java.api.profile_config') - profile_config.setup() - local profile = profile_config.get_active_profile('module1 -> main_class') - -- verify - assert(profile == nil) - end) - - it('when number_of_profiles > 0', function() - mock_io.open = function() - return file_mock - end - - file_mock.read = function(_, readmode) - assert(readmode == '*a') - return '{ "project_path": { "module1 -> main_class": [{ \ - "vm_args": "vm_args1",\ - "prog_args": "prog_args1",\ - "name": "name1",\ - "is_active": false \ - }] } }' - end - - local profile_config = require('java.api.profile_config') - profile_config.setup() - assert.has.error(function() - profile_config.get_active_profile('module1 -> main_class') - end, 'No active profile') - end) - end) -end) diff --git a/tests/java/api/runner_spec.lua b/tests/java/api/runner_spec.lua deleted file mode 100644 index 3707e8f..0000000 --- a/tests/java/api/runner_spec.lua +++ /dev/null @@ -1,713 +0,0 @@ -local spy = require('luassert.spy') -local mock = require('luassert.mock') -local notify = require('java-core.utils.notify') -local DapSetup = require('java-dap.api.setup') -local mock_client = { jdtls_args = {} } -local runner = require('java.api.runner') -local async = require('java-core.utils.async').sync -local profile_config = require('java.api.profile_config') -local ui = require('java.utils.ui') - -local RunnerApi = runner.RunnerApi({ client = mock_client }) - -describe('java-core.api.runner', function() - before_each(function() - package.loaded['java.api.runner'] = nil - package.loaded['java.utils.ui'] = nil - end) - - it('RunnerApi()', function() - local mock_dap = DapSetup(mock_client) - assert.same(RunnerApi.client, mock_client) - assert.same(RunnerApi.dap, mock_dap) - end) - - it('RunnerApi:get_config when no config found', function() - local dap_mock = mock(DapSetup, true) - dap_mock.get_dap_config.returns({}) - - local notify_spy = spy.on(notify, 'warn') - local config = RunnerApi:get_config() - - assert.equals(config, nil) - assert.spy(notify_spy).was_called_with('Config not found') - mock.revert() - end) - - it('RunnerApi:get_config when only one config found', function() - local dap_mock = mock(DapSetup, true) - dap_mock.get_dap_config.returns({ - { name = 'config1', projectName = 'projectName' }, - }) - - local config = RunnerApi:get_config() - assert.same(config, { name = 'config1', projectName = 'projectName' }) - mock.revert() - end) - - it('RunnerApi:get_config when multiple config found', function() - RunnerApi.dap.get_dap_config = function() - return { - { name = 'config1' }, - { name = 'config2', projectName = 'project2' }, - { name = 'config3', projectName = 'project3' }, - } - end - - local mock_ui = mock(vim.ui, true) - mock_ui.select.returns() - - local select_spy = spy.on(vim.ui, 'select') - - async(function() - local config = RunnerApi:get_config() - assert.same({ name = 'config2', projectName = 'project2' }, config) - mock.revert(mock_ui) - end).run() - - assert.same(select_spy.calls[1].vals[1], { 'config2', 'config3' }) - - assert.same( - select_spy.calls[1].vals[2], - { prompt = 'Select the main class (modul -> mainClass)' } - ) - - mock.revert(mock_ui) - end) - - it('RunnerApi:run_app when no config found', function() - RunnerApi.get_config = function() - return nil - end - - local callback_mock = function(_) end - local callback_spy = spy.new(callback_mock) - - RunnerApi:run_app(callback_spy) - assert.spy(callback_spy).was_not_called() - end) - - it('RunnerApi:run_app without active profile', function() - RunnerApi.get_config = function() - return { name = 'config1' } - end - - RunnerApi.dap.enrich_config = function() - return { - classPaths = { 'path1', 'path2' }, - mainClass = 'mainClass', - javaExec = 'javaExec', - } - end - - RunnerApi.profile_config = profile_config - RunnerApi.profile_config.get_active_profile = function(main_class) - assert.equals(main_class, 'mainClass') - return nil - end - - local callback_mock = function(_, _) end - local callback_spy = spy.new(callback_mock) - - RunnerApi:run_app(callback_spy) - assert.spy(callback_spy).was_called_with({ - 'javaExec', - '', -- vm_args - '-cp', - 'path1:path2', - 'mainClass', - '', -- prog_args - }, { name = 'config1', request = 'launch' }) - end) - - it('RunnerApi:run_app with active profile', function() - RunnerApi.get_config = function() - return { name = 'config1' } - end - - RunnerApi.dap.enrich_config = function() - return { - classPaths = { 'path1', 'path2' }, - mainClass = 'mainClass', - javaExec = 'javaExec', - } - end - - RunnerApi.profile_config = profile_config - RunnerApi.profile_config.get_active_profile = function(main_class) - assert.equals(main_class, 'mainClass') - return { - prog_args = 'profile_prog_args', - vm_args = 'vm_args', - } - end - - local callback_mock = function(_, _) end - local callback_spy = spy.new(callback_mock) - - RunnerApi:run_app(callback_spy, 'input_prog_args') - assert.spy(callback_spy).was_called_with({ - 'javaExec', - 'vm_args', - '-cp', - 'path1:path2', - 'mainClass', - 'profile_prog_args input_prog_args', - }, { name = 'config1', request = 'launch' }) - end) - - it('RunningApp:new', function() - local api = mock(vim.api, true) - api.nvim_create_buf.returns(1) - local running_app = runner.RunningApp({ projectName = 'projectName' }) - assert.equals(running_app.win, nil) - assert.equals(running_app.bufnr, 1) - assert.equals(running_app.job_id, nil) - assert.equals(running_app.chan, nil) - assert.same(running_app.dap_config, { projectName = 'projectName' }) - assert.equals(running_app.is_open, false) - assert.equals(running_app.running_status, nil) - mock.revert(api) - end) - - it('BuildInRunner:new', function() - local built_in_main_runner = runner.BuiltInMainRunner() - assert.equals(built_in_main_runner.current_app, nil) - assert.same(built_in_main_runner.running_apps, {}) - end) - - it('BuildInRunner:_set_up_buffer', function() - local vim = mock(vim, true) - vim.wo = {} - vim.wo[1] = {} - local api = mock(vim.api, true) - api.nvim_get_current_win.returns(1) - api.nvim_create_buf.returns(2) - local spy_cmd = spy.on(vim, 'cmd') - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - - spy.on(runner.BuiltInMainRunner, 'set_up_buffer_autocmd') - runner.BuiltInMainRunner.set_up_buffer(running_app) - - assert.equals(running_app.is_open, true) - assert.equals(running_app.win, 1) - assert.spy(spy_cmd).was_called_with('sp | winc J | res 15 | buffer 2') - assert.spy(runner.BuiltInMainRunner.set_up_buffer_autocmd).was_called() - - mock.revert(api) - mock.revert(vim) - end) - - it('BuildInRunner:_set_up_buffer_autocmd', function() - local api = mock(vim.api, true) - api.nvim_create_buf.returns(1) - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - running_app.is_open = true - runner.BuiltInMainRunner.set_up_buffer_autocmd(running_app) - - local call_info = api.nvim_create_autocmd.calls[1] - assert.same(call_info.vals[1], { 'BufHidden' }) - assert.equals(call_info.vals[2].buffer, 1) - - call_info.vals[2].callback() - assert.is_false(running_app.is_open) - - mock.revert(api) - end) - - it('BuiltInMainRunner.on_stdout when is_open=true', function() - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - running_app.chan = 2 - - spy.on(vim.api, 'nvim_buf_call') - runner.BuiltInMainRunner.on_stdout({ 'data1', 'data2' }, running_app) - assert.spy(spy_chensend).was_called_with(2, { 'data1', 'data2' }) - - assert.spy(api.nvim_buf_call).was_not_called() - - mock.revert(api) - end) - - it( - 'BuiltInMainRunner.on_stdout when bufnr is equal to current bufnr and mode is "i" (skip scroll)', - function() - local mock_current_bufnr = 1 - local vim = mock(vim, true) - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - - api.nvim_get_current_buf.returns(mock_current_bufnr) - api.nvim_create_buf.returns(mock_current_bufnr) - api.nvim_get_mode.returns({ mode = 'i' }) - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - running_app.chan = 2 - running_app.is_open = true - -- running_app.bufnr = mock_current_bufnr - spy.on(runner.BuiltInMainRunner, 'scroll_down') - - runner.BuiltInMainRunner.on_stdout({ 'data1', 'data2' }, running_app) - - assert.spy(spy_chensend).was_called_with(2, { 'data1', 'data2' }) - -- -- call nvim_create_buf - local call_info = api.nvim_buf_call.calls[1] - call_info.vals[2]() - - assert.spy(runner.BuiltInMainRunner.scroll_down).was_not_called() - -- - mock.revert(vim) - mock.revert(api) - end - ) - - it( - 'BuiltInMainRunner:_on_stdout when bufnr is not equal to current bufnr and mode is "i" (scroll)', - function() - local vim = mock(vim, true) - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - - api.nvim_get_current_buf.returns(1) - api.nvim_create_buf.returns(3) - api.nvim_get_mode.returns({ mode = 'i' }) - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - running_app.chan = 2 - running_app.is_open = true - -- running_app.bufnr = mock_current_bufnr - spy.on(runner.BuiltInMainRunner, 'scroll_down') - - runner.BuiltInMainRunner.on_stdout({ 'data1', 'data2' }, running_app) - - assert.spy(spy_chensend).was_called_with(2, { 'data1', 'data2' }) - -- -- call nvim_create_buf - local call_info = api.nvim_buf_call.calls[1] - call_info.vals[2]() - - assert - .spy(runner.BuiltInMainRunner.scroll_down) - .was_called_with(running_app) - -- - mock.revert(vim) - mock.revert(api) - end - ) - - it( - 'BuiltInMainRunner:_on_stdout when bufnr is equal to current bufnr and mode is not "i" (scroll)', - function() - local vim = mock(vim, true) - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - - api.nvim_get_current_buf.returns(1) - api.nvim_create_buf.returns(1) - api.nvim_get_mode.returns({ mode = 'n' }) - - local running_app = runner.RunningApp({ projectName = 'projectName' }) - running_app.chan = 2 - running_app.is_open = true - -- running_app.bufnr = mock_current_bufnr - spy.on(runner.BuiltInMainRunner, 'scroll_down') - - runner.BuiltInMainRunner.on_stdout({ 'data1', 'data2' }, running_app) - - assert.spy(spy_chensend).was_called_with(2, { 'data1', 'data2' }) - -- -- call nvim_create_buf - local call_info = api.nvim_buf_call.calls[1] - call_info.vals[2]() - - assert - .spy(runner.BuiltInMainRunner.scroll_down) - .was_called_with(running_app) - -- - mock.revert(vim) - mock.revert(api) - end - ) - - it( - 'BuiltInMainRunner:_on_exit when bufnr is equal to current bufnr (stopinsert)', - function() - local mock_current_bufnr = 1 - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - local spy_cmd = spy.on(vim, 'cmd') - - api.nvim_get_current_buf.returns(mock_current_bufnr) - api.nvim_create_buf.returns(mock_current_bufnr) - - local running_app = - runner.RunningApp({ projectName = 'projectName', name = 'config1' }) - running_app.chan = 2 - running_app.is_open = true - running_app.job_id = 1 - - runner.BuiltInMainRunner.on_exit(0, running_app) - assert - .spy(spy_chensend) - .was_called_with(2, '\nProcess finished with exit code 0\n') - assert.spy(spy_cmd).was_called_with('stopinsert') - assert.equals( - running_app.running_status, - 'Process finished with exit code 0' - ) - - mock.revert(api) - end - ) - - it( - 'BuiltInMainRunner:_on_exit when bufnr is not equal to current bufnr (skip stopinsert)', - function() - local api = mock(vim.api, true) - local spy_chensend = spy.on(vim.fn, 'chansend') - local spy_cmd = spy.on(vim, 'cmd') - - api.nvim_get_current_buf.returns(3) - api.nvim_create_buf.returns(4) - - local running_app = - runner.RunningApp({ projectName = 'projectName', name = 'config1' }) - running_app.chan = 2 - running_app.is_open = true - running_app.job_id = 1 - - runner.BuiltInMainRunner.on_exit(0, running_app) - assert - .spy(spy_chensend) - .was_called_with(2, '\nProcess finished with exit code 0\n') - assert.spy(spy_cmd).was_not_called() - assert.equals( - running_app.running_status, - 'Process finished with exit code 0' - ) - - mock.revert(api) - end - ) - - it('BuiltInMainRunner:run_app when there is no running job', function() - local fn = mock(vim.fn, true) - local spy_jobstart = spy.on(fn, 'jobstart') - local spy_chansend = spy.on(fn, 'chansend') - local api = mock(vim.api, true) - - api.nvim_create_buf.returns(1) - api.nvim_open_term.returns(2) - - local running_app = runner.RunningApp() - running_app.is_open = false - running_app.dap_config = { name = 'config1' } - - local built_in_main_runner = runner.BuiltInMainRunner() - - runner.BuiltInMainRunner.set_up_buffer = function(selected_app) - assert.equals(selected_app, running_app) - end - runner.BuiltInMainRunner.scroll_down = function(selected_app) - assert.equals(selected_app, running_app) - end - - local spy_stop = spy.on(runner.BuiltInMainRunner, 'stop') - - built_in_main_runner.select_app_with_dap_config = function() - return running_app - end - - built_in_main_runner:run_app( - { 'java', '-cp', 'path1:path2', 'mainClass' }, - { name = 'config1' } - ) - - assert.equals(running_app.chan, 2) - assert.equals(running_app.running_status, '(running)') - - assert - .spy(spy_chansend) - .was_called_with(2, 'java -cp path1:path2 mainClass') - assert.stub(api.nvim_buf_call).was_called() - assert.spy(spy_jobstart).was_called() - - local call_info = fn.jobstart.calls[1] - assert.equals(call_info.vals[1], 'java -cp path1:path2 mainClass') - assert.not_nil(call_info.vals[2].on_exit) - assert.not_nil(call_info.vals[2].on_stdout) - assert.spy(spy_stop).was_not_called() - assert.spy(api.nvim_buf_set_name).was_called_with(1, 'config1') - - mock.revert(api) - mock.revert(fn) - end) - - it('BuiltInMainRunner:run_app when there is a running job', function() - local fn = mock(vim.fn, true) - local spy_chensend = spy.on(fn, 'chansend') - local spy_jobstart = spy.on(fn, 'jobstart') - local spy_jobwait = spy.on(fn, 'jobwait') - local spy_stop = spy.on(fn, 'jobstop') - local api = mock(vim.api, true) - - api.nvim_create_buf.returns(1) - api.nvim_open_term.returns(2) - - local running_app = runner.RunningApp() - running_app.is_open = false - running_app.job_id = 1 - running_app.dap_config = { name = 'config1' } - - local built_in_main_runner = runner.BuiltInMainRunner() - built_in_main_runner.current_app = running_app - - runner.BuiltInMainRunner.set_up_buffer = function(selected_app) - assert.equals(selected_app, running_app) - end - runner.BuiltInMainRunner.scroll_down = function(selected_app) - assert.equals(selected_app, running_app) - end - runner.BuiltInMainRunner.change_current_app = function(app) - assert.equals(app.job_id, nil) -- check if job_id is nil - end - - built_in_main_runner.select_app_with_dap_config = function() - return running_app - end - - built_in_main_runner:run_app( - { 'java', '-cp', 'path1:path2', 'mainClass' }, - { name = 'config1' } - ) - - assert.equals(running_app.running_status, '(running)') - assert.is_nil(running_app.job_id) - assert - .spy(spy_chensend) - .was_called_with(2, 'java -cp path1:path2 mainClass') - assert.spy(spy_stop).was_called_with(1) - assert.spy(spy_jobstart).was_called() - assert.spy(spy_jobwait).was_called_with({ 1 }, 1000) - assert.spy(api.nvim_buf_set_name).was_called_with(1, 'config1') - - mock.revert(api) - mock.revert(fn) - end) - - it('BuiltInMainRunner:toggle_logs when is_open=true', function() - local api = mock(vim.api, true) - api.nvim_create_buf.returns(11) - - local running_app = runner.RunningApp() - running_app.is_open = true - - runner.BuiltInMainRunner.hide_logs = function(selected_app) - assert.equals(selected_app, running_app) - end - - local spy_set_up_buffer = spy.on(runner.BuiltInMainRunner, 'set_up_buffer') - local spy_hide_logs = spy.on(runner.BuiltInMainRunner, 'hide_logs') - - runner.BuiltInMainRunner.toggle_logs(running_app) - - assert.spy(spy_hide_logs).was_called() - assert.spy(spy_set_up_buffer).was_not_called() - - mock.revert(api) - end) - - it('BuiltInMainRunner:toggle_logs when is_open=false', function() - local api = mock(vim.api, true) - api.nvim_create_buf.returns(1) - - local running_app = runner.RunningApp() - running_app.is_open = false - - runner.BuiltInMainRunner.set_up_buffer = function(selected_app) - assert.equals(selected_app, running_app) - end - - runner.BuiltInMainRunner.scroll_down = function(selected_app) - assert.equals(selected_app, running_app) - end - - local spy_set_up_buffer = spy.on(runner.BuiltInMainRunner, 'set_up_buffer') - local spy_hide_logs = spy.on(runner.BuiltInMainRunner, 'hide_logs') - - runner.BuiltInMainRunner.toggle_logs(running_app) - - local call_info = api.nvim_buf_call.calls[1] - assert.equals(call_info.vals[1], 1) - call_info.vals[2]() - - assert.spy(spy_hide_logs).was_not_called() - assert.spy(spy_set_up_buffer).was_called() - - mock.revert(api) - end) - - it('BuiltInMainRunner:stop when job_id is nil', function() - local running_app = runner.RunningApp() - running_app.job_id = nil - - local fn_job_stop_spy = spy.on(vim.fn, 'jobstop') - local fn_job_wait_spy = spy.on(vim.fn, 'jobwait') - runner.BuiltInMainRunner.stop(running_app) - - assert.spy(fn_job_stop_spy).was_not_called() - assert.spy(fn_job_wait_spy).was_not_called() - end) - - it('BuiltInMainRunner:stop when job_id is not nil', function() - local running_app = runner.RunningApp() - running_app.job_id = 1 - - local fn_job_stop_spy = spy.on(vim.fn, 'jobstop') - local fn_job_wait_spy = spy.on(vim.fn, 'jobwait') - - runner.BuiltInMainRunner.stop(running_app) - - assert.spy(fn_job_stop_spy).was_called_with(1) - assert.spy(fn_job_wait_spy).was_called_with({ 1 }, 1000) - end) - - it( - 'BuiltInMainRunner:select_from_avaible_apps when no running apps', - function() - local build_in_main_runner = runner.BuiltInMainRunner() - build_in_main_runner.running_apps = {} - - assert.error(function() - build_in_main_runner:select_app_with_ui() - end) - end - ) - - it( - 'BuildInRunner:select_from_running_apps when only one running app', - function() - local build_in_main_runner = runner.BuiltInMainRunner() - local running_app = runner.RunningApp({ projectName = 'projectName' }) - build_in_main_runner.running_apps = { running_app } - - local selected_app = build_in_main_runner:select_app_with_ui() - assert.equals(selected_app, running_app) - end - ) - - it( - 'BuildInRunner:select_from_running_apps when multiple running apps', - function() - local running_app1 = runner.RunningApp({ - projectName = 'projectName1', - mainClass = 'mainClass1', - }) - local running_app2 = runner.RunningApp({ - projectName = 'projectName2', - mainClass = 'mainClass2', - }) - - local build_in_main_runner = runner.BuiltInMainRunner() - build_in_main_runner.running_apps[running_app1.dap_config.mainClass] = - running_app1 - build_in_main_runner.running_apps[running_app2.dap_config.mainClass] = - running_app2 - - build_in_main_runner.change_current_app = function(_) end - - local spy_change_current_app = - spy.on(build_in_main_runner, 'change_current_app') - - ui.select_from_dap_configs = function(configs) - assert(#configs == 2) - return running_app2.dap_config - end - - local result = build_in_main_runner:select_app_with_ui() - assert.equals(result, running_app2) - - assert.spy(spy_change_current_app).was_called() - end - ) - - it('BuildInRunner:select_app_with_dap_config when no config', function() - local build_in_main_runner = runner.BuiltInMainRunner() - assert.error(function() - build_in_main_runner:select_app_with_dap_config() - end) - end) - - it( - 'BuildInRunner:select_app_with_dap_config when config is not found', - function() - local build_in_main_runner = runner.BuiltInMainRunner() - build_in_main_runner.running_apps = {} - - local config = { projectName = 'projectName', mainClass = 'mainClass' } - local selected_app = - build_in_main_runner:select_app_with_dap_config(config) - - assert.equals(selected_app.dap_config, config) - end - ) - - it('BuildInRunner:select_app_with_dap_config when config is found', function() - local build_in_main_runner = runner.BuiltInMainRunner() - build_in_main_runner.running_apps = {} - - local config1 = { projectName = 'projectName1', mainClass = 'mainClass1' } - local config2 = { projectName = 'projectName2', mainClass = 'mainClass2' } - - build_in_main_runner.running_apps[config1.mainClass] = - runner.RunningApp(config1) - build_in_main_runner.running_apps[config2.mainClass] = - runner.RunningApp(config2) - - local selected_app = - build_in_main_runner:select_app_with_dap_config(config2) - assert.equals(selected_app.dap_config, config2) - end) - - it('BuildInRunner:change_current_app', function() - local build_in_main_runner = runner.BuiltInMainRunner() - local running_app1 = runner.RunningApp({ projectName = 'projectName1' }) - local running_app2 = runner.RunningApp({ projectName = 'projectName2' }) - - runner.BuiltInMainRunner.hide_logs = function(selected_app) - assert.equals(selected_app, running_app2) - end - - local spy_hide_logs = spy.on(runner.BuiltInMainRunner, 'hide_logs') - - build_in_main_runner.current_app = running_app1 - - build_in_main_runner.hide_logs = function(selected_app) - assert.equals(selected_app, running_app1) - end - - assert.spy(spy_hide_logs).was_not_called() - build_in_main_runner.current_app = running_app2 - build_in_main_runner:change_current_app(running_app1) - end) - - it('BuildInRunner:change_current_app when dap config is equal', function() - local build_in_main_runner = runner.BuiltInMainRunner() - local running_app1 = runner.RunningApp({ projectName = 'projectName1' }) - - local spy_hide_logs = spy.on(runner.BuiltInMainRunner, 'hide_logs') - - build_in_main_runner.current_app = running_app1 - - build_in_main_runner.hide_logs = function(selected_app) - assert.equals(selected_app, running_app1) - end - - build_in_main_runner:change_current_app(running_app1) - - assert.spy(spy_hide_logs).was_not_called() - build_in_main_runner.current_app = running_app1 - end) -end) diff --git a/tests/java/java_spec.lua b/tests/java/java_spec.lua deleted file mode 100644 index 61a939e..0000000 --- a/tests/java/java_spec.lua +++ /dev/null @@ -1,18 +0,0 @@ -local plugin = require('java') -local mock = require('luassert.mock') - -describe('setup', function() - it('setup function', function() - assert('setup function is available', plugin.setup) - end) - - describe('check runner function available:', function() - local mock_runner = mock(plugin.runner, true) - - it('run_app', function() - mock_runner.run_app.returns({}) - - assert.same(plugin.runner.run_app(), {}) - end) - end) -end) diff --git a/tests/java/ui/profile_spec.lua b/tests/java/ui/profile_spec.lua deleted file mode 100644 index 2c7b700..0000000 --- a/tests/java/ui/profile_spec.lua +++ /dev/null @@ -1,405 +0,0 @@ -local spy = require('luassert.spy') -local Menu = require('nui.menu') -local class = require('java-core.utils.class') - -describe('java.ui.profile', function() - local default_win_options = { - winhighlight = 'Normal:Normal,FloatBorder:Normal', - } - local default_style = 'single' - - -- profile_config mock - local profile_config = { - Profile = function(vm_args, prog_args, name, is_active) - return { - vm_args = vm_args, - prog_args = prog_args, - name = name, - is_active = is_active, - } - end, - get_profile = function() - return {} - end, - get_all_profiles = function() - return {} - end, - } - - -- notify mock - local notify = { - warn = function() end, - error = function() end, - } - - -- dap mock - local dap = { - config_dap = function() end, - } - - -- NuiMenu mock - local MockMenu = class() - function MockMenu:_init(table1, table2) - self.table1 = table1 - self.table2 = table2 - end - function MockMenu.on() end - function MockMenu.unmount() end - function MockMenu.map() end - function MockMenu.mount() end - - before_each(function() - package.loaded['java.api.profile_config'] = nil - package.loaded['java.ui.profile'] = nil - package.loaded['java-core.utils.notify'] = notify - end) - - it('get_tree_node_list_for_menu', function() - local expected_menu_items = { - Menu.item('name2 (active)'), - Menu.item('name'), - Menu.separator(), - Menu.item('New Profile'), - } - - profile_config.get_all_profiles = function() - return { - name = profile_config.Profile('vm_args', 'prog_args', 'name', false), - name2 = profile_config.Profile('vm_args2', 'prog_args2', 'name2', true), - } - end - package.loaded['java.api.profile_config'] = profile_config - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI('main_class') - local items = ui:get_tree_node_list_for_menu() - - for key, value in pairs(items) do - assert.same(expected_menu_items[key].text, value.text) - assert.same(expected_menu_items[key]._type, value._type) - end - end) - - it('get_menu', function() - package.loaded['nui.menu'] = MockMenu - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - ui.get_tree_node_list_for_menu = function() - return { menu_list = 'mock_menu_list' } - end - - local menu = ui:get_menu() - - assert.same(menu.table2.lines, { menu_list = 'mock_menu_list' }) - assert.same(menu.table1.border.text.top, '[Profiles]') - assert.same( - menu.table1.border.text.bottom, - '[a]ctivate [d]elete [b]ack [q]uit' - ) - assert(menu.table2.on_submit ~= nil) - end) - - describe('_get_and_fill_popup', function() - it('successfully', function() - local spy_nvim_api = spy.on(vim.api, 'nvim_buf_set_lines') - - profile_config.get_profile = function(name, main_class) - assert.same(name, 'target_profile') - assert.same(main_class, 'main_class') - return profile_config.Profile('vm_args', 'prog_args', 'name', false) - end - package.loaded['java.api.profile_config'] = profile_config - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI('main_class') - local popup = ui:_get_and_fill_popup('Title', 'name', 'target_profile') - - assert - .spy(spy_nvim_api) - .was_called_with(popup.bufnr, 0, -1, false, { 'name' }) - spy_nvim_api:revert() - - assert.same(popup.border._.text.top._content, '[Title]') - assert.same(popup.border._.text.top_align, 'center') - assert.same(popup.border._.winhighlight, default_win_options.winhighlight) - assert.same(popup.border._.style, default_style) - end) - - it('when target_profile is nil', function() - local spy_nvim_api = spy.on(vim.api, 'nvim_buf_set_lines') - profile_config.get_profile = function(_, _) - return profile_config.Profile('vm_args', 'prog_args', 'name', false) - end - package.loaded['java.api.profile_config'] = profile_config - - local profile_ui = require('java.ui.profile') - profile_ui.ProfileUI() - assert.spy(spy_nvim_api).was_not_called() - spy_nvim_api:revert() - end) - end) - - it('_open_profile_editor', function() - package.loaded['java.api.profile_config'] = profile_config - - -- mock popup - local MockPopup = class() - function MockPopup:_init(options) - self.border = options.border - self.enter = options.enter - self.win_options = options.win_options - self.bufnr = 1 - self.map_list = {} - end - - function MockPopup:map(mode, key, callback) - table.insert( - self.map_list, - { mode = mode, key = key, callback = callback } - ) - end - package.loaded['nui.popup'] = MockPopup - - -- mock layout - local MockLayout = class() - function MockLayout:_init(layout_settings, layout_box_list) - self.layout_settings = layout_settings - self.layout_box_list = layout_box_list - end - - local boxes = {} - function MockLayout.Box(box, options) - table.insert(boxes, { box = box, options = options }) - return {} - end - - function MockLayout.mount() end - local spy_mockLayout_mount = spy.on(MockLayout, 'mount') - - package.loaded['nui.layout'] = MockLayout - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - ui:_open_profile_editor('target_profile') - - -- verify Layout mount call - assert.spy(spy_mockLayout_mount).was_called() - - -- verify Layout.Box calls - assert.same(boxes[1].box.border.text.top, '[Name]') - assert.same(boxes[2].box.border.text.top, '[VM arguments]') - assert.same(boxes[3].box.border.text.top, '[Program arguments]') - assert.same(boxes[3].box.border.text.bottom, '[s]ave [b]ack [q]uit') - assert.same(boxes[3].box.border.text.bottom_align, 'center') - - assert.same(boxes[1].options, { grow = 0.2 }) - assert.same(boxes[2].options, { grow = 0.4 }) - assert.same(boxes[3].options, { grow = 0.4 }) - - -- loop in popup.map calls - for i = 1, 3 do - local list = boxes[i].box.map_list - -- verify keybindings - - -- actions - -- back - assert.same(list[1].mode, 'n') - assert.same(list[1].key, 'b') - assert(list[1].callback ~= nil) - - assert.same(list[2].mode, 'n') - assert.same(list[2].key, 'q') - assert(list[2].callback ~= nil) - - assert.same(list[3].mode, 'n') - assert.same(list[3].key, 's') - assert(list[3].callback ~= nil) - - -- navigation - assert.same(list[4].mode, 'n') - assert.same(list[4].key, '') - assert(list[4].callback ~= nil) - - assert.same(list[5].mode, 'n') - assert.same(list[5].key, 'k') - assert(list[5].callback ~= nil) - - assert.same(list[6].mode, 'n') - assert.same(list[6].key, 'j') - assert(list[6].callback ~= nil) - end - end) - - describe('_set_active_profile', function() - it('successfully', function() - -- mock profile_config - local new_profile = 'mock_new_profile' - profile_config.set_active_profile = function(name) - assert.same(name, new_profile) - end - package.loaded['java.api.profile_config'] = profile_config - package.loaded['java.api.dap'] = dap - - local spy_dap = spy.on(dap, 'config_dap') - local spy_mockMenu = spy.on(MockMenu, 'unmount') - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - -- set up mock in ui - ui.menu = MockMenu() - local openMenu_call = 0 - ui.openMenu = function() - openMenu_call = openMenu_call + 1 - end - ui.focus_item = { text = new_profile } - - -- call function - ui:_set_active_profile() - - --verify - assert.spy(spy_mockMenu).was_called() - assert.spy(spy_dap).was_called() - end) - - it('when selected profile is not modifiable', function() - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - ui._is_selected_profile_modifiable = function() - return false - end - ui:_set_active_profile() - end) - - it('when selected profile active', function() - local spy_notify = spy.on(notify, 'error') - local spy_profile_config = spy.on(profile_config, 'set_active_profile') - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - ui._is_selected_profile_modifiable = function() - return true - end - - ui.focus_item = { text = 'mock (active)' } - ui:_set_active_profile() - assert.spy(spy_notify).was_not_called() - assert.spy(spy_profile_config).was_not_called() - end) - end) - - describe('_delete_profile', function() - it('successfully', function() - -- mock profile_config - local new_profile = 'mock_new_profile' - profile_config.delete_profile = function(name) - assert.same(name, new_profile) - end - package.loaded['java.api.profile_config'] = profile_config - - local spy_mockMenu = spy.on(MockMenu, 'unmount') - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - -- set up mock in ui - ui.menu = MockMenu() - local openMenu_call = 0 - ui.openMenu = function() - openMenu_call = openMenu_call + 1 - end - ui.focus_item = { text = new_profile } - - -- call function - ui:_delete_profile() - - --verify - assert.spy(spy_mockMenu).was_called() - end) - - it('when selected profile is not modifiable', function() - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - ui._is_selected_profile_modifiable = function() - return false - end - ui:_delete_profile() - end) - - it('when selected profile active', function() - local spy_notify = spy.on(notify, 'warn') - local spy_profile_config = spy.on(profile_config, 'delete_profile') - - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - ui.focus_item = { text = 'mock (active)' } - ui:_delete_profile() - assert.spy(spy_notify).was_called_with('Cannot delete active profile') - assert.spy(spy_profile_config).was_not_called() - end) - end) - - describe('_is_selected_profile_modifiable when ', function() - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - - it('focus_item is nil', function() - ui:_is_selected_profile_modifiable() - assert.same(ui:_is_selected_profile_modifiable(), false) - end) - - it('focus_item.text is nil', function() - ui.focus_item = { text = nil } - ui:_is_selected_profile_modifiable() - assert.same(ui:_is_selected_profile_modifiable(), false) - end) - - it('focus_item.text is new_profile', function() - ui.focus_item = { text = 'New Profile' } - ui:_is_selected_profile_modifiable() - assert.same(ui:_is_selected_profile_modifiable(), false) - end) - end) - - it('openMenu', function() - -- mock Menu - local spy_on_mount = spy.on(MockMenu, 'mount') - local spy_on_map = spy.on(MockMenu, 'map') - -- mock profile_ui - local profile_ui = require('java.ui.profile') - local ui = profile_ui.ProfileUI() - ui.get_menu = function() - return MockMenu() - end - - ui:openMenu() - - assert.spy(spy_on_mount).was_called(1) - assert.spy(spy_on_map).was_called(4) - - -- verify keybindings - -- quit - assert.same(spy_on_map.calls[1].refs[2], 'n') - assert.same(spy_on_map.calls[1].refs[3], 'q') - assert(spy_on_map.calls[1].refs[4] ~= nil) - - -- back - assert.same(spy_on_map.calls[2].refs[2], 'n') - assert.same(spy_on_map.calls[2].refs[3], 'b') - assert(spy_on_map.calls[2].refs[4] ~= nil) - - -- set active profile - assert.same(spy_on_map.calls[3].refs[2], 'n') - assert.same(spy_on_map.calls[3].refs[3], 'a') - assert(spy_on_map.calls[3].refs[4] ~= nil) - - -- delete profile - assert.same(spy_on_map.calls[4].refs[2], 'n') - assert.same(spy_on_map.calls[4].refs[3], 'd') - assert(spy_on_map.calls[4].refs[4] ~= nil) - end) -end) diff --git a/tests/prepare-config.lua b/tests/prepare-config.lua deleted file mode 100644 index f2dafac..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, - }, - { - 'williamboman/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..fc0550f --- /dev/null +++ b/tests/specs/capabilities_spec.lua @@ -0,0 +1,27 @@ +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 err = require('java-core.utils.errors') + +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 + err.throw('Additional commands found that are not in required list:', 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..ef1bfc2 --- /dev/null +++ b/tests/utils/prepare-config.lua @@ -0,0 +1,68 @@ +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() + local is_nixos = vim.fn.filereadable('/etc/NIXOS') == 1 + local is_ci = vim.env.CI ~= nil + + 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') + 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..c2cca10 --- /dev/null +++ b/tests/utils/test-config.lua @@ -0,0 +1,38 @@ +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 +local is_ci = vim.env.CI ~= nil + +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')