diff --git a/.luacheckrc b/.luacheckrc index 4bf3a7d..1ed5747 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -4,6 +4,9 @@ codes = true self = false +max_code_line_length = 125 +max_comment_line_length = false + read_globals = { "vim", } diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..1215223 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "Lua.diagnostics.disable": ["undefined-doc-param", "doc-field-no-class"] +} diff --git a/Makefile b/Makefile index 944b00e..a442743 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,6 @@ format: lint: luacheck lua + +gendoc: + nvim --headless --noplugin -u scripts/minimal_doc.vim -c "luafile ./scripts/generate_doc.lua" -c 'qa' diff --git a/doc/java_util.txt b/doc/java_util.txt new file mode 100644 index 0000000..285e983 --- /dev/null +++ b/doc/java_util.txt @@ -0,0 +1,31 @@ +================================================================================ +LSP *java_util.lsp* + +The Java Util lsp module exposes a collection of functions that extend or +change the functionality of the standard java language server + +The behaviour of a lot of these functions are partly inspired by how IntelliJ +functions + +lsp.rename({new_name}, {opts}) *java_util.lsp.rename()* + Rename what you are currently hovering. If you are hovering a field and you + are using lombok, it will also rename your getters and setters to the + chosen new name. + + + Parameters: ~ + {new_name} (string|nil) If new_name is passed you will not be + prompted for a new new_name + {opts} (table|nil) options which will be passed to the + |vim.lsp.buf.rename| + + Options: ~ + {filter} (function|nil) Predicate to filter clients used for rename. + Receives the attached clients as argument and + must return a list of clients. + {name} (string|nil) Restrict clients used for rename to ones + where client.name matches this field. + + + + vim:tw=78:ts=8:ft=help:norl: diff --git a/doc/tags b/doc/tags new file mode 100644 index 0000000..5bb1289 --- /dev/null +++ b/doc/tags @@ -0,0 +1,2 @@ +java_util.lsp java_util.txt /*java_util.lsp* +java_util.lsp.rename() java_util.txt /*java_util.lsp.rename()* diff --git a/lua/java_util/lsp/init.lua b/lua/java_util/lsp/init.lua new file mode 100644 index 0000000..1485f5b --- /dev/null +++ b/lua/java_util/lsp/init.lua @@ -0,0 +1,32 @@ +---@tag java_util.lsp + +---@config { ['field_heading'] = "Options", ["module"] = "java_util.lsp" } + +---@brief [[ +--- The Java Util lsp module exposes a collection of functions that extend or change the functionality of the standard java language server +--- +--- The behaviour of a lot of these functions are partly inspired by how IntelliJ functions +---@brief ]] + +local lsp = {} + +-- Ref: https://github.com/tjdevries/lazy.nvim +local function require_on_exported_call(mod) + return setmetatable({}, { + __index = function(_, picker) + return function(...) + return require(mod)[picker](...) + end + end, + }) +end + +--- Rename what you are currently hovering. +--- If you are hovering a field and you are using lombok, it will also rename your getters and setters to the chosen new name. +---@param new_name string|nil: If new_name is passed you will not be prompted for a new new_name +---@param opts table|nil: options which will be passed to the |vim.lsp.buf.rename| +---@field filter function|nil: Predicate to filter clients used for rename. Receives the attached clients as argument and must return a list of clients. +---@field name string|nil: Restrict clients used for rename to ones where client.name matches this field. +lsp.rename = require_on_exported_call("java_util.lsp.internal.rename").rename + +return lsp diff --git a/lua/java_util/lsp/internal/rename.lua b/lua/java_util/lsp/internal/rename.lua new file mode 100644 index 0000000..41e3e4f --- /dev/null +++ b/lua/java_util/lsp/internal/rename.lua @@ -0,0 +1,108 @@ +local string_util = require("java_util.string_util") +local lsp_util = require("java_util.lsp.util") +local ts_utils = require("nvim-treesitter.ts_utils") + +local rename = {} + +local function is_field(node) + local first_parent = node:parent() + + if first_parent:type() == "field_access" then + return true + end + + if first_parent:parent():type() == "field_declaration" then + return true + end + + return false +end + +function rename.rename(new_name, opts) + opts = opts or {} + + 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 }) + return + end + end + + vim.lsp.buf.rename(new_name, opts) +end + +local function with_name(new_name, callback) + local old_name = vim.fn.expand("") + if new_name ~= nil then + callback(new_name, old_name) + else + vim.ui.input({ prompt = "New Name:", default = old_name }, function(n_name) + if n_name then + callback(n_name, old_name) + end + end) + end +end + +function rename._rename_field(opts) + local params = vim.lsp.util.make_position_params(0) + params.context = { includeDeclaration = true } + lsp_util.request_all({ + { bufnr = 0, method = "textDocument/references", params = params }, + { bufnr = 0, method = "textDocument/definition", params = params }, + }, function(results) + local references = results["textDocument/references"][1] + local def_results = results["textDocument/definition"] + local definitions = def_results[1] + + if #references == 0 and #definitions == 0 then + return + end + + -- If we are currently on the definition then textDocument/definition will not provide it to using + -- So we have to create the entry ourselves + local is_on_definition = #definitions == 0 + if is_on_definition then + local context = def_results[2] + local range = ts_utils.node_to_lsp_range(opts.node_at_cursor) + table.insert(references, { + uri = context.params.textDocument.uri, + range = range, + }) + else + for _, def in ipairs(definitions) do + table.insert(references, def) + end + end + + with_name(opts.new_name, function(new_name, old_name) + local uppercase_old_name = string_util.first_to_upper(old_name) + local uppercase_new_name = string_util.first_to_upper(new_name) + local getter = string.format("%s%s", "get", uppercase_old_name) + local setter = string.format("%s%s", "set", uppercase_old_name) + + for _, reference in ipairs(references) do + local bufnr = vim.uri_to_bufnr(reference.uri) + vim.fn.bufload(bufnr) + local start = reference.range.start + local the_end = reference.range["end"] + local line = vim.api.nvim_buf_get_text(bufnr, start.line, start.character, the_end.line, the_end.character, {})[1] + local original_len = string.len(line) + + if string_util.starts_with(line, getter) or string_util.starts_with(line, setter) then + local beginning = string.sub(line, 0, 3) + local ending = string.sub(line, string.len(string.format("%s%s", beginning, uppercase_old_name)) + 1) + line = string.format("%s%s%s", beginning, uppercase_new_name, ending) + else + local ending = string.sub(line, string.len(old_name) + 1) + line = string.format("%s%s", new_name, ending) + end + + vim.api.nvim_buf_set_text(bufnr, start.line, start.character, start.line, start.character + original_len, { line }) + end + end) + end) +end + +return rename diff --git a/lua/java_util/lsp/util.lua b/lua/java_util/lsp/util.lua new file mode 100644 index 0000000..6f14b05 --- /dev/null +++ b/lua/java_util/lsp/util.lua @@ -0,0 +1,21 @@ +local lsp_util = {} + +function lsp_util.request_all(requests, handler) + local completed = {} + local completed_count = 0 + for _, request in ipairs(requests) do + vim.lsp.buf_request(request.bufnr, request.method, request.params, function(err, ...) + if err then + vim.api.nvim_err_writeln(string.format("Error when requesting method '%s': %s", request.method, err.message)) + return + end + completed[request.method] = { ... } + completed_count = completed_count + 1 + if completed_count == #requests then + handler(completed) + end + end) + end +end + +return lsp_util diff --git a/lua/java_util/string_util.lua b/lua/java_util/string_util.lua new file mode 100644 index 0000000..48f38b2 --- /dev/null +++ b/lua/java_util/string_util.lua @@ -0,0 +1,11 @@ +local string_util = {} + +function string_util.first_to_upper(str) + return str:gsub("^%l", string.upper) +end + +function string_util.starts_with(str, pattern) + return str:find("^" .. pattern) ~= nil +end + +return string_util diff --git a/lua/tests/automated/lsp/util_spec.lua b/lua/tests/automated/lsp/util_spec.lua new file mode 100644 index 0000000..f139f39 --- /dev/null +++ b/lua/tests/automated/lsp/util_spec.lua @@ -0,0 +1,60 @@ +local mock = require("luassert.mock") + +describe("util", function() + local util = require("java_util.lsp.util") + + describe("request_all", function() + local mock_lsp + + after_each(function() + mock.revert(mock_lsp) + end) + + local function was_called(params, fn) + mock_lsp = mock(vim.lsp, true) + mock_lsp.buf_request.invokes(fn) + local called = false + util.request_all(params, function() + called = true + end) + return called + end + + it("given 4 requests when all are succesful then call the callback", function() + local called = was_called({ + { bufnr = 0, method = "textDocument/references", params = {} }, + { bufnr = 0, method = "textDocument/references", params = {} }, + { bufnr = 0, method = "textDocument/references", params = {} }, + { bufnr = 0, method = "textDocument/definition", params = {} }, + }, function(_, _, _, fn) + fn() + end) + + assert.is_true(called) + end) + + it("given 2 requests when one fails then dont call the callback", function() + local count = 0 + local called = was_called({ + { bufnr = 0, method = "textDocument/references", params = {} }, + { bufnr = 0, method = "textDocument/definition", params = {} }, + }, function(_, _, _, fn) + if count < 1 then + fn() + end + count = count + 1 + end) + + assert.is_false(called) + end) + + it("given 2 requests when all fails then dont call the callback", function() + local called = was_called({ + { bufnr = 0, method = "textDocument/references", params = {} }, + { bufnr = 0, method = "textDocument/definition", params = {} }, + }, function(_, _, _, _) end) + + assert.is_false(called) + end) + end) +end) diff --git a/scripts/generate_doc.lua b/scripts/generate_doc.lua new file mode 100644 index 0000000..a7e3f84 --- /dev/null +++ b/scripts/generate_doc.lua @@ -0,0 +1,35 @@ +local docgen = require("docgen") + +local docs = {} + +docs.test = function() + -- Filepaths that should generate docs + local input_files = { + "./lua/java_util/lsp/init.lua", + } + + -- Maybe sort them that depends what you want and need + table.sort(input_files, function(a, b) + return #a < #b + end) + + -- Output file + local output_file = "./doc/java_util.txt" + local output_file_handle = io.open(output_file, "w") + + for _, input_file in ipairs(input_files) do + docgen.write(input_file, output_file_handle) + end + + if not output_file_handle then + return + end + + output_file_handle:write(" vim:tw=78:ts=8:ft=help:norl:\n") + output_file_handle:close() + vim.cmd([[checktime]]) +end + +docs.test() + +return docs diff --git a/scripts/minimal_doc.vim b/scripts/minimal_doc.vim new file mode 100644 index 0000000..984bb45 --- /dev/null +++ b/scripts/minimal_doc.vim @@ -0,0 +1,7 @@ +set rtp+=. + +set rtp+=../plenary.nvim +set rtp+=../tree-sitter-lua/ + +runtime! plugin/plenary.vim +runtime! plugin/ts_lua.vim diff --git a/stylua.toml b/stylua.toml index 6090f42..17058c3 100644 --- a/stylua.toml +++ b/stylua.toml @@ -1,4 +1,4 @@ -column_width = 120 +column_width = 125 line_endings = "Unix" indent_type = "Spaces" indent_width = 2