Skip to content

Commit

Permalink
Go to Test and implementation from test
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-z authored Aug 18, 2022
2 parents b8997a3 + 5e3eefc commit dd8d889
Show file tree
Hide file tree
Showing 16 changed files with 284 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ""
labels: feature
labels: enhancement
assignees: ""
---

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ snapshots:
sh ./scripts/build-snapshot.sh CreateTestStringClassSnippets create_test_string_class_snippets
sh ./scripts/build-snapshot.sh CreateTestLuasnipClassSnippets create_test_luasnip_class_snippets
sh ./scripts/build-snapshot.sh CreateTestAfterSnippetRun create_test_after_snippet_run
sh ./scripts/build-snapshot.sh GotoTest goto_test
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The plugin is tested against [Neovim (v0.7.0)](https:/neovim/neovim/

- Jdtls setup using either [lsp-config](https:/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#jdtls) or [nvim-jdtls](https:/mfussenegger/nvim-jdtls). (or anything that uses the built in neovim lsp)
- [nvim-treesitter](https:/nvim-treesitter/nvim-treesitter)
- [Plenary](https:/nvim-lua/plenary.nvim)

### Installation

Expand All @@ -46,7 +47,8 @@ use({
-- or branch = "dev"
-- or tag = "0.1.0"
requires = {
{ "nvim-treesitter/nvim-treesitter" }
{ "nvim-treesitter/nvim-treesitter" },
{ "nvim-lua/plenary.nvim" }
}
})
```
Expand Down Expand Up @@ -87,10 +89,11 @@ For more in depth information about specific functions see `:help java_util`

### LSP

| Functions | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lsp.rename` | Renames the word you are hovering. Supports lombok renaming |
| `lsp.create_test` | Creates a test class for the current class you are in. Allows for multiple test class configurations. For more information see [Creating Tests](https:/tobias-z/java-util.nvim/wiki/Creating-Tests) |
| Functions | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lsp.rename` | Renames the word you are hovering. Supports lombok renaming |
| `lsp.create_test` | Creates a test class for the current class you are in. Allows for multiple test class configurations. For more information see [Test Management](https:/tobias-z/java-util.nvim/wiki/Test-Management) |
| `lsp.goto_test` | Bidirectional test movement, will either to go test or main class, depending on where you are. For more information see [Going to Tests](https:/tobias-z/java-util.nvim/wiki/Test-Management#going-to-tests) |

## Configuration

Expand Down
31 changes: 31 additions & 0 deletions doc/java_util.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,36 @@ lsp.create_test({opts}) *java_util.lsp.create_test()*
testname.


lsp.goto_test({opts}) *java_util.lsp.goto_test()*
Bidirectional test movement
1. If you are in a none test class, it will go to the test of your current
class. If multiple are found, a prompt is shown for you to choose
2. If you are in a test class, it will go the the class you testing. If a
direct match is found, it will take you directly there otherwise, it
will prompt you with a list of possible matches. An example of a direct
match would be, you are currently in a class called UserServiceImplTest.
Here a direct match would be if a class is found called UserServiceImpl


Parameters: ~
{opts} (table|nil) options to specify the goto_test behaviour.

Options: ~
{on_no_results} (function|nil) Called if no results are found. Could
be used to create a test if no class
is found
{current_class} (string|nil) Specifies the class used to search for
tests/classes. Default is the current
class you are in. If empty string is
passed, it will show all tests found
from the `opts.cwd`
{filter} (function|nil) Predicate to filter the test results.
Could be used if there are some tests
you always don't want to see.
{cwd} (string|nil) Specifies the cwd you want to search
from. Default is the src root of the
current module or project you are in.



vim:tw=78:ts=8:ft=help:norl:
11 changes: 11 additions & 0 deletions lua/java_util/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,15 @@ lsp.rename = require_on_exported_call("java_util.lsp.internal.rename").rename
---@field testname string|nil: The name of the test you want to create. This will skip the prompt to select a testname.
lsp.create_test = require_on_exported_call("java_util.lsp.internal.create_test").create_test

--- Bidirectional test movement
--- 1. If you are in a none test class, it will go to the test of your current class. If multiple are found, a prompt is shown for you to choose
--- 2. If you are in a test class, it will go the the class you testing. If a direct match is found, it will take you directly there otherwise, it will prompt you with a list of possible matches.
--- An example of a direct match would be, you are currently in a class called UserServiceImplTest. Here a direct match would be if a class is found called UserServiceImpl
---@param opts table|nil: options to specify the goto_test behaviour.
---@field on_no_results function|nil: Called if no results are found. Could be used to create a test if no class is found
---@field current_class string|nil: Specifies the class used to search for tests/classes. Default is the current class you are in. If empty string is passed, it will show all tests found from the `opts.cwd`
---@field filter function|nil: Predicate to filter the test results. Could be used if there are some tests you always don't want to see.
---@field cwd string|nil: Specifies the cwd you want to search from. Default is the src root of the current module or project you are in.
lsp.goto_test = require_on_exported_call("java_util.lsp.internal.goto_test").goto_test

return lsp
20 changes: 2 additions & 18 deletions lua/java_util/lsp/internal/create_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,6 @@ local values = require("java_util.config").values

local create_test = {}

local function up_directory(path)
return vim.fn.fnamemodify(path, ":h")
end

local function get_src_root(path)
while not vim.endswith(path, "/src") do
path = up_directory(path)

if path == "/" then
error("unable to find src root")
end
end

return path
end

--- Changes /src/main to /src/test in the closest src_root to the current class
local function get_test_location(src_root, filepath)
local root_split = vim.split(string.sub(src_root, 2), "/")
Expand Down Expand Up @@ -114,8 +98,8 @@ function create_test.create_test(opts)
local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())
local default_filename = string.format("%sTest", string.sub(bufname, string.find(bufname, "/[^/]*$") + 1, -6))

local removed_filename = up_directory(bufname)
local src_root = get_src_root(removed_filename)
local removed_filename = lsp_util.up_directory(bufname)
local src_root = lsp_util.get_src_root(removed_filename)
local location = get_test_location(src_root, removed_filename)

with_filepath({
Expand Down
146 changes: 146 additions & 0 deletions lua/java_util/lsp/internal/goto_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
local lsp_util = require("java_util.lsp.util")
local plenary_util = require("java_util.plenary_util")
local string_util = require("java_util.string_util")

local goto_test = {}

--- Sorts the classes by classname length, smallest should be on top
--- So the closest match is always the top one
local function get_sorted_classes(classes)
local keys = vim.tbl_keys(classes)
return vim.fn.sort(keys, function(c1, c2)
local c1_len = string.len(string.sub(c1, string_util.last_index_of(c1, "/") + 1))
local c2_len = string.len(string.sub(c2, string_util.last_index_of(c2, "/") + 1))
if c1_len > c2_len then
return 1
elseif c1_len == c2_len then
return 0
else
return -1
end
end)
end

---@param opts table
---@param classes table: key is shortened classpath, value is full path
local function handle_results(opts, classes)
local found_len = vim.tbl_count(classes)
if found_len == 0 then
if opts.on_no_results then
opts.on_no_results(opts)
end
return
elseif found_len == 1 then
local first = classes[vim.tbl_keys(classes)[1]]
lsp_util.jump_to_file(string.format("file://%s", first))
else
vim.ui.select(
get_sorted_classes(classes),
{ prompt = string.format("Choose class (%d, found)", found_len) },
function(item)
local choosen = classes[item]
lsp_util.jump_to_file(string.format("file://%s", choosen))
end
)
end
end

local function get_search_classes(classname)
local searches = {}
local cur = ""
for c in classname:gmatch(".") do
local ascii = string.byte(c)
local is_uppercase = ascii >= 65 and ascii <= 90
if is_uppercase then
if cur ~= "" then
table.insert(searches, cur)
end
end
cur = cur .. c
end
table.insert(searches, classname)

-- Only search for 50% of the class names
-- UserServiceImpl becomes { "UserServiceImpl", "UserService" } and ignores the User part
local search_items_rounded = math.floor((vim.tbl_count(searches) / 2) + 0.5)
local results = {}
for i = search_items_rounded, 1, -1 do
table.insert(results, searches[i + 1])
end

if vim.tbl_count(results) == 0 then
table.insert(results, classname)
end

return results
end

local function with_found_classes(opts, callback)
local cwd = vim.fn.getcwd()
local test_classes = {}
local args = {
opts.cwd,
"-type",
"f",
}

local searches = get_search_classes(opts.current_class)
for _, search_item in ipairs(searches) do
table.insert(args, "-name")
table.insert(args, string.format("%s*%s", search_item, opts.is_test and "" or "Test.java"))
end
plenary_util.execute_with_results({
cmd = "find",
cwd = opts.cwd,
args = args,
process_result = function(line)
-- make the result start from src/...
local shortened = string.sub(line, string.len(cwd) + 2)
if opts.filter and type(opts.filter) == "function" then
if not opts.filter(shortened) then
return
end
end

test_classes[shortened] = line
end,
process_complete = function()
vim.schedule(function()
callback(opts, test_classes)
end)
end,
})
end

function goto_test.goto_test(opts)
opts = opts or {}
local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())
local src_root = lsp_util.get_src_root(bufname)
local combined_opts = vim.tbl_extend("force", {
current_class = string.sub(bufname, string.find(bufname, "/[^/]*$") + 1, -6),
is_test = vim.startswith(string.sub(bufname, string.len(src_root) + 1), "/test"),
}, opts)

if combined_opts.is_test then
combined_opts.cwd = string.format("%s/main", src_root)
if vim.endswith(combined_opts.current_class, "Test") then
combined_opts.current_class = string.sub(combined_opts.current_class, 0, -5)
end
with_found_classes(combined_opts, function(_, classes)
-- If we found a resulting class called the same as test class without Test in the end, we will simply use that as out result
for key, result in pairs(classes) do
if vim.endswith(result, string.format("%s.java", combined_opts.current_class)) then
handle_results(combined_opts, { [key] = result })
return
end
end

handle_results(combined_opts, classes)
end)
else
combined_opts.cwd = string.format("%s/test", src_root)
with_found_classes(combined_opts, handle_results)
end
end

return goto_test
4 changes: 2 additions & 2 deletions lua/java_util/lsp/internal/rename.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function rename.rename(new_name, opts)
if vim.o.filetype == "java" then
local node_at_cursor = ts_utils.get_node_at_cursor(0)
if is_field(node_at_cursor) then
rename._rename_field({ new_name = new_name, opts = opts, node_at_cursor = node_at_cursor })
rename.__rename_field({ new_name = new_name, opts = opts, node_at_cursor = node_at_cursor })
return
end
end
Expand All @@ -49,7 +49,7 @@ local function with_name(new_name, callback)
end
end

function rename._rename_field(opts)
function rename.__rename_field(opts)
local params = vim.lsp.util.make_position_params(0)
params.context = { includeDeclaration = true }
lsp_util.request_all({
Expand Down
24 changes: 24 additions & 0 deletions lua/java_util/lsp/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,31 @@ function lsp_util.get_main_package(src_root, file_location)
return get_package(src_root, file_location, false)
end

function lsp_util.up_directory(path)
return vim.fn.fnamemodify(path, ":h")
end

function lsp_util.get_src_root(path)
while not vim.endswith(path, "/src") do
path = lsp_util.up_directory(path)

if path == "/" then
error("unable to find src root")
end
end

return path
end

--- Checks if the uri is already loaded in a buffer. If it is it will just load it.
--- If the uri is not loaded it will open it
function lsp_util.jump_to_file(uri)
local bufnr = vim.uri_to_bufnr(uri)
if vim.api.nvim_buf_is_loaded(bufnr) then
vim.api.nvim_win_set_buf(vim.api.nvim_get_current_win(), bufnr)
return
end

vim.lsp.util.jump_to_location({
range = {
start = {
Expand Down
29 changes: 29 additions & 0 deletions lua/java_util/plenary_util.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
local Job = require("plenary.job")

local plenary_util = {}

---Executes any command through plenary
---@param opts table: the options
---@field cmd string: the command you want to run
---@field cwd string|nil: the current working directory to run the command from
---@field process_result function: will be called whenever the process outputs something
---@field process_complete function: will be called when the process is finished
function plenary_util.execute_with_results(opts)
Job:new({
command = opts.cmd,
cwd = opts.cwd,
args = opts.args,
on_stdout = function(_, line, _)
if not line or line == "" then
return
end

opts.process_result(line)
end,
on_exit = function()
opts.process_complete()
end,
}):start()
end

return plenary_util
4 changes: 4 additions & 0 deletions lua/java_util/string_util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ function string_util.first_to_upper(str)
return str:gsub("^%l", string.upper)
end

function string_util.last_index_of(str, pattern)
return string.match(str, "^.*()" .. pattern)
end

return string_util
15 changes: 15 additions & 0 deletions lua/tests/java_util/automated/lsp/goto_test_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
local helpers = require("tests.java_util.util.helpers")

local ok = assert.is_true

describe("goto_test", function()
it("can go to a test from none test class", function()
ok(helpers.snapshot_matches(
"GotoTest",
[[
package io.github.tobiasz.testproject.builders;
public class GotoTestTest {}]]
))
end)
end)
3 changes: 3 additions & 0 deletions lua/tests/java_util/builders/goto_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require("java_util").setup({})

require("java_util.lsp").goto_test()
3 changes: 3 additions & 0 deletions lua/tests/java_util/snapshots/GotoTest.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.tobiasz.testproject.builders;

public class GotoTestTest {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.tobiasz.testproject.builders;

public class GotoTest {}
Loading

0 comments on commit dd8d889

Please sign in to comment.