nvim-jdtls/lua/jdtls.lua

1304 lines
39 KiB
Lua

---@mod jdtls LSP extensions for Neovim and eclipse.jdt.ls
local api = vim.api
local ui = require('jdtls.ui')
local util = require('jdtls.util')
local with_java_executable = util.with_java_executable
local with_classpaths = util.with_classpaths
local resolve_classname = util.resolve_classname
local execute_command = util.execute_command
local jdtls_dap = require('jdtls.dap')
local setup = require('jdtls.setup')
local offset_encoding = 'utf-16'
---@diagnostic disable-next-line: deprecated
local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
local M = {
setup_dap = jdtls_dap.setup_dap,
test_class = jdtls_dap.test_class,
test_nearest_method = jdtls_dap.test_nearest_method,
pick_test = jdtls_dap.pick_test,
extendedClientCapabilities = setup.extendedClientCapabilities,
setup = setup,
settings = {
jdt_uri_timeout_ms = 5000,
}
}
--- Start the language server (if not started), and attach the current buffer.
---
---@param config table<string, any> configuration. See |vim.lsp.start_client|
---@param opts? jdtls.start.opts
---@param start_opts? vim.lsp.start.Opts options passed to vim.lsp.start
---@return integer|nil client_id
function M.start_or_attach(config, opts, start_opts)
return setup.start_or_attach(config, opts, start_opts)
end
local request = function(bufnr, method, params, handler)
local clients = get_clients({ bufnr = bufnr, name = "jdtls" })
local _, client = next(clients)
if not client then
vim.notify("No LSP client with name `jdtls` available", vim.log.levels.WARN)
return
end
local co
if not handler then
co = coroutine.running()
if co then
handler = function(err, result, ctx)
coroutine.resume(co, err, result, ctx)
end
end
end
client.request(method, params, handler, bufnr)
if co then
return coroutine.yield()
end
end
local highlight_ns = api.nvim_create_namespace('jdtls_hl')
M.jol_path = nil
local function java_apply_workspace_edit(command)
for _, argument in ipairs(command.arguments) do
vim.lsp.util.apply_workspace_edit(argument, offset_encoding)
end
end
local function java_generate_to_string_prompt(_, outer_ctx)
local params = outer_ctx.params
local bufnr = assert(outer_ctx.bufnr, '`outer_ctx` must have bufnr property')
coroutine.wrap(function()
local err, result = request(bufnr, 'java/checkToStringStatus', params)
if err then
print("Could not execute java/checkToStringStatus: " .. err.message)
return
end
if not result then
return
end
if result.exists then
local prompt = string.format(
"Method 'toString()' already exists in '%s'. Do you want to replace it?",
result.type
)
local choice = ui.pick_one({"Replace", "Cancel"}, prompt, tostring)
if choice == "Cancel" then
return
end
end
local fields = ui.pick_many(result.fields, 'Include item in toString?', function(x)
return string.format('%s: %s', x.name, x.type)
end)
local e, edit = request(bufnr, 'java/generateToString', { context = params; fields = fields; })
if e then
print("Could not execute java/generateToString: " .. e.message)
elseif edit then
vim.lsp.util.apply_workspace_edit(edit, offset_encoding)
end
end)()
end
local function java_generate_constructors_prompt(_, outer_ctx)
local bufnr = assert(outer_ctx.bufnr, '`outer_ctx` must have bufnr property')
coroutine.wrap(function()
local err0, status = request(bufnr, 'java/checkConstructorsStatus', outer_ctx.params)
if err0 then
print("Could not execute java/checkConstructorsStatus: " .. err0.message)
return
end
if not status or not status.constructors or #status.constructors == 0 then
return
end
local constructors = status.constructors
if #status.constructors > 1 then
constructors = ui.pick_many(status.constructors, 'Include super class constructor(s): ', function(x)
return string.format('%s(%s)', x.name, table.concat(x.parameters, ','))
end)
if not constructors or #constructors == 0 then
return
end
end
local fields = status.fields
if fields then
local opts = {
is_selected = function(item)
return item.isSelected
end
}
fields = ui.pick_many(status.fields, 'Include field to initialize by constructor(s): ', function(x)
return string.format('%s: %s', x.name, x.type)
end, opts)
end
local params = {
context = outer_ctx.params,
constructors = constructors,
fields = fields
}
local err1, edit = request(bufnr, 'java/generateConstructors', params)
if err1 then
print("Could not execute java/generateConstructors: " .. err1.message)
elseif edit then
vim.lsp.util.apply_workspace_edit(edit, offset_encoding)
end
end)()
end
local function java_generate_delegate_methods_prompt(_, outer_ctx)
local bufnr = assert(outer_ctx.bufnr, '`outer_ctx` must have bufnr property')
coroutine.wrap(function()
local err0, status = request(bufnr, 'java/checkDelegateMethodsStatus', outer_ctx.params)
if err0 then
print('Could not execute java/checkDelegateMethodsStatus: ', err0.message)
return
end
if not status or not status.delegateFields or #status.delegateFields == 0 then
print('All delegatable methods are already implemented.')
return
end
local field = #status.delegateFields == 1 and status.delegateFields[1] or ui.pick_one(
status.delegateFields,
'Select target to generate delegates for.',
function(x) return string.format('%s: %s', x.field.name, x.field.type) end
)
if not field then
return
end
if #field.delegateMethods == 0 then
print('All delegatable methods are already implemented.')
return
end
local methods = ui.pick_many(field.delegateMethods, 'Generate delegate for method:', function(x)
return string.format('%s(%s)', x.name, table.concat(x.parameters, ','))
end)
if not methods or #methods == 0 then
return
end
local params = {
context = outer_ctx.params,
delegateEntries = vim.tbl_map(
function(x)
return {
field = field.field,
delegateMethod = x
}
end,
methods
),
}
local err1, workspace_edit = request(bufnr, 'java/generateDelegateMethods', params)
if err1 then
print('Could not execute java/generateDelegateMethods', err1.message)
elseif workspace_edit then
vim.lsp.util.apply_workspace_edit(workspace_edit, offset_encoding)
end
end)()
end
local function java_hash_code_equals_prompt(_, outer_ctx)
local bufnr = assert(outer_ctx.bufnr, '`outer_ctx` must have bufnr property')
local params = outer_ctx.params
coroutine.wrap(function()
local _, result = request(bufnr, 'java/checkHashCodeEqualsStatus', params)
if not result then
vim.notify("No result", vim.log.levels.INFO)
return
elseif not result.fields or #result.fields == 0 then
vim.notify(string.format("The operation is not applicable to the type %", result.type), vim.log.levels.WARN)
return
end
local fields = ui.pick_many(result.fields, 'Include item in equals/hashCode?', function(x)
return string.format('%s: %s', x.name, x.type)
end)
local err, edit = request(bufnr, 'java/generateHashCodeEquals', { context = params; fields = fields; })
if err then
print("Could not execute java/generateHashCodeEquals: " .. err.message)
elseif edit then
vim.lsp.util.apply_workspace_edit(edit, offset_encoding)
end
end)()
end
local function handle_refactor_workspace_edit(err, result, ctx)
if err then
print('Error getting refactoring edit: ' .. err.message)
return
end
if not result then
return
end
if result.edit then
vim.lsp.util.apply_workspace_edit(result.edit, offset_encoding)
end
if result.command then
local command = result.command
local fn = M.commands[command.command]
if fn then
fn(command, ctx)
else
execute_command(command)
end
end
end
local function move_file(command, code_action_params)
local uri = command.arguments[3].uri
local params = {
moveKind = 'moveResource';
sourceUris = { uri, },
params = vim.NIL
}
request(0, 'java/getMoveDestinations', params, function(err, result, ctx)
assert(not err, err and err.message or vim.inspect(err))
if result and result.errorMessage then
print(result.errorMessage)
return
end
if not result or not result.destinations or #result.destinations == 0 then
print("Couldn't find any destination packages")
return
end
local destinations = vim.tbl_filter(
function(x) return not x.isDefaultPackage end,
result.destinations
)
ui.pick_one_async(
destinations,
'Target package> ',
function(x)
local name = x.project .. ' » ' .. (x.isParentOfSelectedFile and '* ' or '') .. x.displayName
local sourceset = string.match(x.path, "src/(%a+)/")
return (sourceset and sourceset or x.path) .. " » " .. name
end,
function(x)
local move_params = {
moveKind = 'moveResource',
sourceUris = { uri, },
params = code_action_params,
destination = x,
updateReferences = true
}
request(ctx.bufnr, 'java/move', move_params, function(move_err, refactor_edit)
handle_refactor_workspace_edit(move_err, refactor_edit)
end)
end
)
end)
end
local function move_instance_method(command, code_action_params)
local params = {
moveKind = 'moveInstanceMethod';
sourceUris = { command.arguments[2].textDocument.uri, };
params = code_action_params
}
request(0, 'java/getMoveDestinations', params, function(err, result, ctx)
assert(not err, err and err.message or vim.inspect(err))
if result and result.errorMessage then
print(result.errorMessage)
return
end
if not result or not result.destinations or #result.destinations == 0 then
print("Couldn't find any destinations")
return
end
ui.pick_one_async(
result.destinations,
'Destination> ',
function(x)
local prefix
if x.isField then
prefix = '[Field] '
else
prefix = '[Method Parameter] '
end
return prefix .. x.type .. ' ' .. x.name
end,
function(x)
params.destination = x
params.updateReferences = true
request(ctx.bufnr, 'java/move', params, function(move_err, refactor_edit)
handle_refactor_workspace_edit(move_err, refactor_edit)
end)
end
)
end)
end
local function search_symbols(project, enclosing_type_name, on_selection)
local params = {
query = '*',
projectName = project,
sourceOnly = true,
}
request(0, 'java/searchSymbols', params, function(err, result, ctx)
assert(not err, err and err.message or vim.inspect(err))
if not result or #result == 0 then
print("Couldn't find any destinations")
return
end
if enclosing_type_name then
result = vim.tbl_filter(
function(x)
if x.containerName then
return enclosing_type_name == x.containerName .. '.' .. x.name
else
return enclosing_type_name == x.name
end
end,
result
)
end
ui.pick_one_async(
result,
'Destination> ',
function(x) return x.containerName .. ' » ' .. x.name end,
function(x)
on_selection(x, ctx.bufnr)
end
)
end)
end
local function move_static_member(command, code_action_params)
local member = command.arguments[3]
search_symbols(
member.projectName,
member.enclosingTypeName,
function(picked, bufnr)
local move_params = {
moveKind = 'moveStaticMember',
sourceUris = { command.arguments[2].uri },
params = code_action_params,
destination = picked
}
request(bufnr, 'java/move', move_params, function(move_err, refactor_edit)
handle_refactor_workspace_edit(move_err, refactor_edit)
end)
end
)
end
local function move_type(command, code_action_params)
local info = command.arguments[3]
if not info.supportedDestinationKinds or #info.supportedDestinationKinds == 0 then
print('No available destinations')
return
end
ui.pick_one_async(
info.supportedDestinationKinds,
'Action> ',
function(x)
if x == 'newFile' then
return string.format('Move type `%s` to new file', info.displayName)
else
return string.format('Move type `%s` to another class', info.displayName)
end
end,
function(x)
if x == 'newFile' then
local move_params = {
moveKind = 'moveTypeToNewFile',
sourceUris = { command.arguments[2].textDocument.uri },
params = code_action_params
}
request(0, 'java/move', move_params, function(move_err, refactor_edit)
handle_refactor_workspace_edit(move_err, refactor_edit)
end)
else
search_symbols(
info.projectName,
info.enclosingTypeName,
function(picked, bufnr)
local move_params = {
moveKind = 'moveTypeToClass',
sourceUris = { command.arguments[2].uri },
params = code_action_params,
destination = picked
}
request(bufnr, 'java/move', move_params, function(move_err, refactor_edit)
handle_refactor_workspace_edit(move_err, refactor_edit)
end)
end
)
end
end
)
end
---@return {tabSize: integer, insertSpaces: boolean}
local function format_opts()
return {
tabSize = vim.lsp.util.get_effective_tabstop(),
insertSpaces = vim.bo.expandtab,
}
end
---@param bufnr integer
---@param command table
---@param code_action_params table
local function change_signature(bufnr, command, code_action_params)
local cmd_name = command.arguments[1]
local signature = command.arguments[3]
local edit_buf = api.nvim_create_buf(false, true)
api.nvim_create_autocmd("BufUnload", {
buffer = edit_buf,
once = true,
callback = function(args)
local lines = api.nvim_buf_get_lines(args.buf, 0, -1, true)
local is_delegate = false
local access_type = signature.modifier
local method_name = signature.methodName
local return_type = signature.returnType
local preview = false
local expect_param_next = false
local parameters = {}
local new_param_idx = #signature.parameters
for _, line in ipairs(lines) do
if vim.startswith(line, "---") then
break
elseif expect_param_next and vim.startswith(line, "- ") then
local matches = { line:match("%- ((%d+):) ([^ ]+) (%w+)") }
if next(matches) then
table.insert(parameters, {
name = matches[4],
originalIndex = assert(tonumber(matches[2]), "Parameter must have originalIndex"),
type = matches[3],
})
else
matches = { line:match("%- (%w+) ([^ ]+) ?(.*)") }
if next(matches) then
table.insert(parameters, {
type = matches[1],
name = matches[2],
defaultValue = matches[3],
originalIndex = new_param_idx
})
new_param_idx = new_param_idx + 1
end
end
elseif vim.startswith(line, "Access type: ") then
access_type = line:sub(#"Access type: " + 1)
elseif vim.startswith(line, "Name: ") then
method_name = line:sub(#"Name: " + 1)
elseif vim.startswith(line, "Parameters:") then
expect_param_next = true
elseif vim.startswith(line, "Return type: ") then
return_type = line:sub(#"Return type: " + 1)
end
end
local params = {
command = cmd_name,
context = code_action_params,
options = format_opts(),
commandArguments = {
signature.methodIdentifier,
is_delegate,
method_name,
access_type,
return_type,
parameters,
signature.exceptions,
preview
},
}
request(bufnr, 'java/getRefactorEdit', params, handle_refactor_workspace_edit)
end,
})
vim.bo[edit_buf].bufhidden = "wipe"
local width = math.floor(vim.o.columns * 0.9)
local height = math.floor(vim.o.lines * 0.8)
local win_opts = {
relative = "editor",
style = "minimal",
row = math.floor((vim.o.lines - height) * 0.5),
col = math.floor((vim.o.columns - width) * 0.5),
width = width,
height = height,
border = "single",
}
api.nvim_open_win(edit_buf, true, win_opts)
local lines = {
"Access type: " .. signature.modifier,
"Name: " .. signature.methodName,
"Return type: " .. signature.returnType,
"Parameters:",
}
for _, param in ipairs(signature.parameters) do
table.insert(lines, string.format("- %d: %s %s",
param.originalIndex,
param.type,
param.name
))
end
local comment_start = #lines + 1
vim.list_extend(lines, {
"",
string.rep("-", math.max(width, 3)),
"Labels are used to parse the values. Keep them!",
"Accept change & close the window:",
" - `<Ctrl-w> q`",
" - `:bd`",
"",
"Parameters:",
" - Order sensitive",
" - New param format: '- <type> <name> [defaultValue]'",
" - Existing param format: '- <n>: <type> <name>'",
" - <n> marks the original index, don't add it for new entries, don't change for moved params",
})
api.nvim_buf_set_lines(edit_buf, 0, -1, true, lines)
local highlights = {
{0, "Access type:", "Identifier"},
{1, "Name:", "Identifier"},
{2, "Return type:", "Identifier"},
{3, "Parameters:", "Identifier"},
}
for _, hl in ipairs(highlights) do
api.nvim_buf_set_extmark(edit_buf, highlight_ns, hl[1], 0, {
end_row = hl[1],
end_col = #hl[2],
hl_group = hl[3],
})
end
api.nvim_buf_set_extmark(edit_buf, highlight_ns, comment_start, 0, {
hl_group = "Comment",
end_row = #lines
})
end
---@param after_refactor? function
local function java_apply_refactoring_command(command, outer_ctx, after_refactor)
local cmd = command.arguments[1]
local bufnr = outer_ctx.bufnr
local code_action_params = outer_ctx.params
if cmd == 'moveFile' then
return move_file(command, code_action_params)
elseif cmd == 'moveInstanceMethod' then
return move_instance_method(command, code_action_params)
elseif cmd == 'moveStaticMember' then
return move_static_member(command, code_action_params)
elseif cmd == 'moveType' then
return move_type(command, code_action_params)
elseif cmd == "changeSignature" then
return change_signature(bufnr, command, code_action_params)
end
local params = {
command = cmd,
context = code_action_params,
options = format_opts(),
}
local apply_refactor = function(err, result, ctx)
handle_refactor_workspace_edit(err, result, ctx)
if after_refactor then
after_refactor()
end
end
if not vim.tbl_contains(setup.extendedClientCapabilities.inferSelectionSupport, cmd) then
request(bufnr, 'java/getRefactorEdit', params, apply_refactor)
return
end
local range = code_action_params.range
if not (range.start.character == range['end'].character and range.start.line == range['end'].line) then
request(bufnr, 'java/getRefactorEdit', params, apply_refactor)
return
end
request(bufnr, 'java/inferSelection', params, function(err, selection_info, ctx)
assert(not err, vim.inspect(err))
if not selection_info or #selection_info == 0 then
print('No selection found that could be extracted')
return
end
if #selection_info == 1 then
params.commandArguments = selection_info
request(ctx.bufnr, 'java/getRefactorEdit', params, apply_refactor)
else
ui.pick_one_async(
selection_info,
'Choices:',
function(x) return x.name end,
function(selection)
if not selection then return end
params.commandArguments = {selection}
request(ctx.bufnr, 'java/getRefactorEdit', params, apply_refactor)
end
)
end
end)
end
local function java_action_rename(command, ctx)
local target = command.arguments[1]
local win = api.nvim_get_current_win()
local bufnr = api.nvim_win_get_buf(win)
if bufnr ~= ctx.bufnr then
return
end
local lines = vim.api.nvim_buf_get_lines(ctx.bufnr, 0, -1, true)
local content = table.concat(lines, '\n')
local byteidx = vim.fn.byteidx(content, target.offset)
local line = vim.fn.byte2line(byteidx)
local col = byteidx - vim.fn.line2byte(line)
api.nvim_win_set_cursor(win, { line, col + 1 })
end
local function java_action_organize_imports(_, ctx)
request(0, 'java/organizeImports', ctx.params, function(err, resp)
if err then
print('Error on organize imports: ' .. err.message)
return
end
if resp then
vim.lsp.util.apply_workspace_edit(resp, offset_encoding)
end
end)
end
local function find_last(str, pattern)
local idx = nil
while true do
local i = string.find(str, pattern, (idx or 0) + 1)
if i == nil then
break
else
idx = i
end
end
return idx
end
local function java_choose_imports(resp)
local uri = resp[1]
local selections = resp[2]
local choices = {}
for _, selection in ipairs(selections) do
local start = selection.range.start
local buf = vim.uri_to_bufnr(uri)
api.nvim_win_set_buf(0, buf)
api.nvim_win_set_cursor(0, {start.line + 1, start.character})
api.nvim_command('normal! zvzz')
api.nvim_buf_add_highlight(
0, highlight_ns, 'IncSearch', start.line, start.character, selection.range['end'].character)
api.nvim_command("redraw")
local candidates = selection.candidates
local fqn = candidates[1].fullyQualifiedName
local type_name = fqn:sub(find_last(fqn, '%.') + 1)
local choice = #candidates == 1 and candidates[1] or ui.pick_one(
candidates,
'Choose type ' .. type_name .. ' to import',
function(x) return x.fullyQualifiedName end
)
api.nvim_buf_clear_namespace(0, highlight_ns, 0, -1)
table.insert(choices, choice)
end
return choices
end
local function java_override_methods(_, context)
local bufnr = assert(context.bufnr, '`context` must have bufnr property')
coroutine.wrap(function()
local err1, result1 = request(bufnr, 'java/listOverridableMethods', context.params)
if err1 then
vim.notify("Error getting overridable methods: " .. err1.message, vim.log.levels.WARN)
return
end
if not result1 or not result1.methods then
vim.notify("No methods to override", vim.log.levels.INFO)
return
end
local fmt = function(method)
return string.format("%s(%s) class: %s", method.name, table.concat(method.parameters, ", "), method.declaringClass)
end
local selected = ui.pick_many(result1.methods, "Method to override", fmt)
if #selected < 1 then
return
end
local params = {
context = context.params,
overridableMethods = selected
}
local err2, result2 = request(context.bufnr, 'java/addOverridableMethods', params)
if err2 ~= nil then
print("Error getting workspace edits: " .. err2.message)
return
end
if result2 then
vim.lsp.util.apply_workspace_edit(result2, offset_encoding)
end
end)()
end
M.commands = {
['java.apply.workspaceEdit'] = java_apply_workspace_edit;
['java.action.generateToStringPrompt'] = java_generate_to_string_prompt;
['java.action.hashCodeEqualsPrompt'] = java_hash_code_equals_prompt;
['java.action.applyRefactoringCommand'] = java_apply_refactoring_command;
['java.action.rename'] = java_action_rename;
['java.action.organizeImports'] = java_action_organize_imports;
['java.action.organizeImports.chooseImports'] = java_choose_imports;
['java.action.generateConstructorsPrompt'] = java_generate_constructors_prompt;
['java.action.generateDelegateMethodsPrompt'] = java_generate_delegate_methods_prompt;
['java.action.overrideMethodsPrompt'] = java_override_methods;
['_java.test.askClientForChoice'] = function(args)
local prompt = args[1]
local choices = args[2]
local pick_many = args[3]
return require("jdtls.tests")._ask_client_for_choice(prompt, choices, pick_many)
end,
['_java.test.advancedAskClientForChoice'] = function(args)
local prompt = args[1]
local choices = args[2]
-- local advanced_action = args[3]
local pick_many = args[4]
return require("jdtls.tests")._ask_client_for_choice(prompt, choices, pick_many)
end,
['_java.test.askClientForInput'] = function(args)
local prompt = args[1]
local default = args[2]
local result = vim.fn.input({
prompt = prompt .. ': ',
default = default
})
return result and result or vim.NIL
end,
}
if vim.lsp.commands then
for k, v in pairs(M.commands) do
vim.lsp.commands[k] = v -- luacheck: ignore 122
end
end
if not vim.lsp.handlers['workspace/executeClientCommand'] then
vim.lsp.handlers['workspace/executeClientCommand'] = function(_, params, ctx) -- luacheck: ignore 122
local client = vim.lsp.get_client_by_id(ctx.client_id) or {}
local commands = client.commands or {}
local global_commands = vim.lsp.commands or M.commands
local fn = commands[params.command] or global_commands[params.command]
if fn then
local ok, result = pcall(fn, params.arguments, ctx)
if ok then
return result
else
return vim.lsp.rpc_response_error(vim.lsp.protocol.ErrorCodes.InternalError, result)
end
else
return vim.lsp.rpc_response_error(
vim.lsp.protocol.ErrorCodes.MethodNotFound,
'Command ' .. params.command .. ' not supported on client'
)
end
end
end
local function make_code_action_params(from_selection)
local params
if from_selection then
params = vim.lsp.util.make_given_range_params()
else
params = vim.lsp.util.make_range_params()
end
params.context = {
diagnostics = {}
}
return params
end
--- Organize the imports in the current buffer
function M.organize_imports()
java_action_organize_imports(nil, { params = make_code_action_params(false) })
end
---@private
function M._complete_compile()
return 'full\nincremental'
end
local function on_build_result(err, result, ctx)
local CompileWorkspaceStatus = {
FAILED = 0,
SUCCEED = 1,
WITHERROR = 2,
CANCELLED = 3,
}
assert(not err, 'Error trying to build project(s): ' .. vim.inspect(err))
if result == CompileWorkspaceStatus.SUCCEED then
vim.fn.setqflist({}, 'r', { title = 'jdtls'; items = {} })
print('Compile successful')
else
local project_config_errors = {}
local compile_errors = {}
local ns = vim.lsp.diagnostic.get_namespace(ctx.client_id)
for _, d in pairs(vim.diagnostic.get(nil, { namespace = ns })) do
local fname = api.nvim_buf_get_name(d.bufnr)
local stat = vim.loop.fs_stat(fname)
local items
if (vim.endswith(fname, 'build.gradle')
or vim.endswith(fname, 'pom.xml')
or (stat and stat.type == 'directory')) then
items = project_config_errors
elseif vim.fn.fnamemodify(fname, ':e') == 'java' then
items = compile_errors
end
if d.severity == vim.diagnostic.severity.ERROR and items then
table.insert(items, d)
end
end
local items = #project_config_errors > 0 and project_config_errors or compile_errors
vim.fn.setqflist({}, 'r', { title = 'jdtls'; items = vim.diagnostic.toqflist(items) })
if #items > 0 then
local reverse_status = {
[0] = "FAILED",
[1] = "SUCCEEDED",
[2] = "WITHERROR",
[3] = "CANCELLED",
}
print(string.format('Compile error. (%s)', reverse_status[result]))
vim.cmd('copen')
else
print("Compile error, but no error diagnostics available."
.. " Save all pending changes and try running compile again."
.. " If you used incremental mode, try a full rebuild.")
end
end
end
--- Compile the Java workspace
--- If there are compile errors they'll be shown in the quickfix list.
---@param type string|nil
---|"full"
---|"incremental"
function M.compile(type)
request(0, 'java/buildWorkspace', type == 'full', on_build_result)
end
---@param mode nil|"prompt"|"all"
local function pick_projects(mode)
local command = {
command = 'java.project.getAll',
}
local bufnr = api.nvim_get_current_buf()
assert(coroutine.running(), '`pick_projects` must be called within coroutine')
local err, projects = util.execute_command(command, nil, bufnr)
if err then
error(err.message or vim.inspect(err))
end
local selection
if mode == "all" then
selection = projects
elseif #projects == 1 then
selection = projects
else
selection = ui.pick_many(
projects,
'Projects> ',
function(project)
return project
end
)
end
return selection
end
--- Trigger a rebuild of one or more projects.
---
---@param opts JdtBuildProjectOpts|nil optional configuration options
function M.build_projects(opts)
opts = opts or {}
local bufnr = api.nvim_get_current_buf()
coroutine.wrap(function()
local selection = pick_projects(opts.select_mode or "prompt")
if selection and next(selection) then
local params = {
identifiers = vim.tbl_map(function(project) return { uri = project } end, selection),
isFullBuild = opts.full_build == nil and true or opts.full_build
}
request(bufnr, 'java/buildProjects', params, on_build_result)
end
end)()
end
---@class JdtBuildProjectOpts
---@field select_mode? JdtProjectSelectMode Show prompt to select projects or select all. Defaults to "prompt"
---@field full_build? boolean full rebuild or incremental build. Defaults to true (full build)
--- Update the project configuration (from Gradle or Maven).
--- In a multi-module project this will only update the configuration of the
--- module of the current buffer.
function M.update_project_config()
local params = { uri = vim.uri_from_bufnr(0) }
request(0, 'java/projectConfigurationUpdate', params, function(err)
if err then
print('Could not update project configuration: ' .. err.message)
end
end)
end
--- Process changes made to the Gradle or Maven configuration of one or more projects.
--- Requires eclipse.jdt.ls >= 1.13.0
---
---@param opts JdtUpdateProjectsOpts|nil configuration options
function M.update_projects_config(opts)
opts = opts or {}
coroutine.wrap(function()
local bufnr = api.nvim_get_current_buf()
local selection = pick_projects(opts.select_mode or "prompt")
if selection and next(selection) then
local params = {
identifiers = vim.tbl_map(function(project) return { uri = project } end, selection)
}
vim.lsp.buf_notify(bufnr, 'java/projectConfigurationsUpdate', params)
end
end)()
end
---@class JdtUpdateProjectsOpts
---@field select_mode? JdtProjectSelectMode show prompt to select projects or select all. Defaults to "prompt"
---@alias JdtProjectSelectMode string
---|"all"
---|"prompt"
---@alias jdtls.extract.opts {visual?: boolean, name?: string|fun(): string}
---@param entity string
---@param opts? jdtls.extract.opts
local function extract(entity, opts)
opts = opts or {}
if type(opts) == "boolean" then
-- bwc, param changed from boolean to table
opts = {
visual = opts
}
end
local params = make_code_action_params(opts.visual or false)
local command = { arguments = { entity }, }
local after_refactor = function()
local name = opts.name
if type(name) == "function" then
name = name()
end
if type(name) == "string" then
vim.lsp.buf.rename(name, { name = "jdtls" })
end
end
java_apply_refactoring_command(command, { params = params }, after_refactor)
end
--- Extract a constant from the expression under the cursor
---@param opts? jdtls.extract.opts
function M.extract_constant(opts)
extract('extractConstant', opts)
end
--- Extract a variable from the expression under the cursor
---@param opts? jdtls.extract.opts
function M.extract_variable(opts)
extract('extractVariable', opts)
end
--- Extract a local variable from the expression under the cursor and replace all occurrences
---@param opts? jdtls.extract.opts
function M.extract_variable_all(opts)
extract('extractVariableAllOccurrence', opts)
end
--- Extract a method
---@param opts? jdtls.extract.opts
function M.extract_method(opts)
extract('extractMethod', opts)
end
--- Jump to the super implementation of the method under the cursor
function M.super_implementation()
local params = {
type = 'superImplementation',
position = vim.lsp.util.make_position_params(0, offset_encoding),
}
request(0, 'java/findLinks', params, function(err, result)
assert(not err, vim.inspect(err))
if result and #result == 1 then
vim.lsp.util.jump_to_location(result[1], offset_encoding, true)
else
assert(result == nil or #result == 0, 'Expected one or zero results for `findLinks`')
vim.notify('No result found')
end
end)
end
--- Run the `javap` tool in a terminal buffer.
--- Sets the classpath based on the current project.
function M.javap()
with_classpaths(function(resp)
local classname = resolve_classname()
local cp = table.concat(resp.classpaths, ':')
local buf = api.nvim_create_buf(false, true)
api.nvim_win_set_buf(0, buf)
vim.fn.termopen({'javap', '-c', '--class-path', cp, classname})
end)
end
--- Run the `jshell` tool in a terminal buffer.
--- Sets the classpath based on the current project.
function M.jshell()
local bufnr = api.nvim_get_current_buf()
with_classpaths(function(result)
local buf = api.nvim_create_buf(true, true)
local classpaths = {}
for _, path in pairs(result.classpaths) do
if vim.fn.filereadable(path) == 1 or vim.fn.isdirectory(path) == 1 then
table.insert(classpaths, path)
end
end
local cp = table.concat(classpaths, ':')
with_java_executable(resolve_classname(), '', function(java_exec)
api.nvim_win_set_buf(0, buf)
local jshell = java_exec and (vim.fn.fnamemodify(java_exec, ":p:h") .. '/jshell') or "jshell"
vim.fn.termopen(jshell, { env = { ["CLASSPATH"] = cp }})
end, bufnr)
end)
end
--- Run the `jol` tool in a terminal buffer to print the class layout
--- You must configure `jol_path` to point to the `jol` jar file:
---
--- ```
--- require('jdtls').jol_path = '/absolute/path/to/jol.jar`
--- ```
---
--- https://github.com/openjdk/jol
---
--- Must be called from a regular java source file.
---
--- Examples:
--- ```
--- lua require('jdtls').jol()
--- ```
---
--- ```
--- lua require('jdtls').jol(nil, "java.util.ImmutableCollections$List12")
--- ```
---@param mode? string
---|"estimates"
---|"footprint"
---|"externals"
---|"internals"
---@param classname? string fully qualified class name. Defaults to the current class.
function M.jol(mode, classname)
mode = mode or 'estimates'
local jol = assert(M.jol_path, [[Path to jol must be set using `lua require('jdtls').jol_path = 'path/to/jol.jar'`]])
with_classpaths(function(resp)
local resolved_classname = resolve_classname()
local cp = table.concat(resp.classpaths, ':')
with_java_executable(resolved_classname, '', function(java_exec)
local buf = api.nvim_create_buf(false, true)
api.nvim_win_set_buf(0, buf)
vim.fn.termopen({
java_exec, '-Djdk.attach.allowAttachSelf', '-jar', jol, mode, '-cp', cp, classname or resolved_classname})
end)
end)
end
--- Open `jdt://` uri or decompile class contents and load them into the buffer
---
--- nvim-jdtls by defaults configures a `BufReadCmd` event which uses this function.
--- You shouldn't need to call this manually.
---
---@param fname string
function M.open_classfile(fname)
local uri
local use_cmd
if vim.startswith(fname, "jdt://") then
uri = fname
use_cmd = false
else
uri = vim.uri_from_fname(fname)
use_cmd = true
if not vim.startswith(uri, "file://") then
return
end
end
local buf = api.nvim_get_current_buf()
vim.bo[buf].modifiable = true
vim.bo[buf].swapfile = false
vim.bo[buf].buftype = 'nofile'
-- This triggers FileType event which should fire up the lsp client if not already running
vim.bo[buf].filetype = 'java'
local timeout_ms = M.settings.jdt_uri_timeout_ms
vim.wait(timeout_ms, function()
return next(get_clients({ name = "jdtls", bufnr = buf })) ~= nil
end)
local client = get_clients({ name = "jdtls", bufnr = buf })[1]
assert(client, 'Must have a `jdtls` client to load class file or jdt uri')
local content
local function handler(err, result)
assert(not err, vim.inspect(err))
content = result
local normalized = string.gsub(result, '\r\n', '\n')
local source_lines = vim.split(normalized, "\n", { plain = true })
api.nvim_buf_set_lines(buf, 0, -1, false, source_lines)
vim.bo[buf].modifiable = false
end
if use_cmd then
local command = {
command = "java.decompile",
arguments = { uri }
}
execute_command(command, handler)
else
local params = {
uri = uri
}
client.request("java/classFileContents", params, handler, buf)
end
-- Need to block. Otherwise logic could run that sets the cursor to a position
-- that's still missing.
vim.wait(timeout_ms, function() return content ~= nil end)
end
---@private
function M._complete_set_runtime()
local client
for _, c in pairs(get_clients()) do
if c.config.settings.java then
client = c
break
end
end
if not client then
return {}
end
local runtimes = (client.config.settings.java.configuration or {}).runtimes or {}
return table.concat(vim.tbl_map(function(runtime) return runtime.name end, runtimes), '\n')
end
--- Change the Java runtime.
--- This requires a `settings.java.configuration.runtimes` configuration.
---
---@param runtime nil|string Java runtime. Prompts for runtime if nil
function M.set_runtime(runtime)
local client
for _, c in pairs(get_clients()) do
if c.config.settings.java then
client = c
break
end
end
if not client then
vim.notify('No LSP client found with settings for java', vim.log.levels.ERROR)
return
end
local runtimes = (client.config.settings.java.configuration or {}).runtimes or {}
if #runtimes == 0 then
vim.notify(
'No runtimes found in `config.settings.java.configuration.runtimes`. You need to add runtime paths to change the runtime',
vim.log.levels.WARN
)
return
end
if runtime then
local match = false
for _, r in pairs(runtimes) do
if r.name == runtime then
r.default = true
match = true
else
r.default = nil
end
end
if not match then
vim.notify(
'Provided runtime `' .. runtime .. '` not found in `config.settings.java.configuration.runtimes`',
vim.log.levels.WARN
)
return
end
client.notify('workspace/didChangeConfiguration', { settings = client.config.settings })
else
ui.pick_one_async(
runtimes,
'Runtime> ',
function(r)
return r.name .. ' (' .. r.path .. ')'
end,
function(selected_runtime)
if not selected_runtime then
return
end
selected_runtime.default = true
for _, r in pairs(runtimes) do
if r ~= selected_runtime then
r.default = nil
end
end
client.notify('workspace/didChangeConfiguration', { settings = client.config.settings })
end
)
end
end
return M