Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): <Plug> mappings for hover actions #398

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,18 +246,41 @@ Start a GHCi repl for the current project / buffer.
Inspired by [rust-tools.nvim](https://github.com/simrat39/rust-tools.nvim),
this plugin adds the following hover actions (if available):

- Hoogle search.
- Open documentation in browser.
- Open source in browser.
- Go to definition.
- Go to type definition.
- Find references.
- Hoogle search
(no dedicated `<Plug>` mapping
because there can be multiple signatures to search for).
- Open documentation in browser (`<Plug>HaskellHoverActionDocs`).
- Open source in browser (`<Plug>HaskellHoverActionSource`).
- Go to definition (`<Plug>HaskellHoverActionDefinition`).
- Go to type definition (`<Plug>HaskellHoverActionTypeDefinition`).
- Find references (`<Plug>HaskellHoverActionReferences`).

You can invoke them by switching to the hover window and entering `<CR>`
on the respective line, or with a keymap for the respective `<Plug>` mapping.
If you do not wish to create a separate keymap for each `<Plug>` mapping,
you can also create a keymap for `<Plug>HaskellHoverAction`, which accepts
a `<count>` prefix as the (1-based) index of the hover action to invoke.

For example, if you set the following keymap:

```lua
vim.keymap.set('n', '<space>a', '<Plug>HaskellHoverAction')
```

you can invoke the third hover action with `3<space>a`.

Additionally, the default behaviour of stylizing markdown is disabled.
And the hover buffer's filetype is set to markdown,
so that [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)
users can benefit from syntax highlighting of code snippets.

For invoking hover actions with a keymap in normal mode, this plugin
provides the following `<Plug>` mappings:

- `HaskellHoverAction`: Takes a `<count>` prefix for the hover action to invoke.
- `HaskellHoverActionHoogle`: Invokes the first Hoogle search hover action.
- `HaskellHoverActionOpenDocs`: Open

![hoverActions](https://user-images.githubusercontent.com/12857160/219763211-61fc4207-4300-41f2-99c4-6a420cf940f2.gif)

- [x] **Automatically generate tags**
Expand Down
111 changes: 85 additions & 26 deletions lua/haskell-tools/lsp/hover.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,30 @@ local function close_hover()
end
end

---Execute the command at the cursor position
---@retrun nil
local function run_command()
local winnr = vim.api.nvim_get_current_win()
local line = vim.api.nvim_win_get_cursor(winnr)[1]
---@param bufnr number
local function delete_plug_mappings(bufnr)
vim
.iter({
'',
'Docs',
'Source',
'Definition',
'TypeDefinition',
'References',
})
:map(function(suffix)
return '<Plug>HaskellHoverAction' .. suffix
end)
:each(function(mapping)
pcall(function()
vim.keymap.del('n', mapping, { buffer = bufnr })
end)
end)
end

---Execute the command at the cursor position
---@param line integer
local function run_command(line)
if line > #_state.commands then
return
end
Expand Down Expand Up @@ -109,6 +127,13 @@ local function is_same_position(params, result)
and params.position.character == range_start.character
end

---@param suffix string
---@param action function
---@param bufnr integer
local function create_plug_mapping(suffix, action, bufnr)
vim.keymap.set('n', '<Plug>HaskellHoverAction' .. suffix, action, { buffer = bufnr, noremap = true, silent = true })
end

---LSP handler for textDocument/hover
---@param result table
---@param ctx table
Expand All @@ -133,6 +158,7 @@ function hover.on_hover(_, result, ctx, config)
vim.notify('No information available')
return
end
local current_bufnr = vim.api.nvim_get_current_buf()
local to_remove = {}
local actions = {}
_state.commands = {}
Expand All @@ -141,10 +167,12 @@ function hover.on_hover(_, result, ctx, config)
local _, signatures = HtParser.try_get_signatures_from_markdown(func_name, result.contents.value)
for _, signature in pairs(signatures) do
table.insert(actions, 1, string.format('%d. Hoogle search: `%s`', #actions + 1, signature))
table.insert(_state.commands, function()
local action = function()
log.debug { 'Hover: Hoogle search for signature', signature }
ht.hoogle.hoogle_signature { search_term = signature }
end)
close_hover()
end
table.insert(_state.commands, action)
end
local cword = vim.fn.expand('<cword>')
table.insert(actions, 1, string.format('%d. Hoogle search: `%s`', #actions + 1, cword))
Expand All @@ -163,23 +191,31 @@ function hover.on_hover(_, result, ctx, config)
table.insert(to_remove, 1, i)
table.insert(actions, 1, string.format('%d. Open documentation in browser', #actions + 1))
local uri = string.match(value, '%[Documentation%]%((.+)%)')
table.insert(_state.commands, function()
local action = function()
log.debug { 'Hover: Open documentation in browser', uri }
OS.open_browser(uri)
end)
close_hover()
end
table.insert(_state.commands, action)
create_plug_mapping('Docs', action, current_bufnr)
elseif vim.startswith(value, '[Source]') and not found_source then
found_source = true
table.insert(to_remove, 1, i)
table.insert(actions, 1, string.format('%d. View source in browser', #actions + 1))
local uri = string.match(value, '%[Source%]%((.+)%)')
table.insert(_state.commands, function()
local action = function()
log.debug { 'Hover: View source in browser', uri }
OS.open_browser(uri)
end)
close_hover()
end
table.insert(_state.commands, action)
create_plug_mapping('Source', action, current_bufnr)
end
local location = string.match(value, '*Defined [ia][nt] (.+)')
local current_file = params.textDocument.uri:gsub('file://', '')
local results, err, definition_results
---@type function
local references_action -- defined here because of the goto statements
if location == nil or found_location then
goto SkipDefinition
end
Expand All @@ -196,7 +232,7 @@ function hover.on_hover(_, result, ctx, config)
if not is_same_position(params, definition_result) then
log.debug { 'Hover: definition location', location_suffix }
table.insert(actions, 1, string.format('%d. Go to definition at ' .. location_suffix, #actions + 1))
table.insert(_state.commands, function()
local action = function()
-- We don't call vim.lsp.buf.definition() because the location params may have changed
local definition_ctx = vim.tbl_extend('force', ctx, {
method = 'textDocument/definition',
Expand All @@ -205,7 +241,10 @@ function hover.on_hover(_, result, ctx, config)
---Neovim 0.9 has a bug in the lua doc
---@diagnostic disable-next-line: param-type-mismatch
vim.lsp.handlers['textDocument/definition'](nil, definition_result, definition_ctx)
end)
close_hover()
end
table.insert(_state.commands, action)
create_plug_mapping('Definition', action, current_bufnr)
end
else -- Display Hoogle search instead
local pkg = location:match('‘(.+)’')
Expand All @@ -217,13 +256,16 @@ function hover.on_hover(_, result, ctx, config)
end)
end
table.insert(actions, 1, string.format('%d. Find references', #actions + 1))
table.insert(_state.commands, function()
references_action = function()
local reference_params = vim.tbl_deep_extend('force', params, { context = { includeDeclaration = true } })
log.debug { 'Hover: Find references', reference_params }
-- We don't call vim.lsp.buf.references() because the location params may have changed
---@diagnostic disable-next-line: missing-parameter
vim.lsp.buf_request(0, 'textDocument/references', reference_params)
end)
close_hover()
end
table.insert(_state.commands, references_action)
create_plug_mapping('Referenes', references_action, current_bufnr)
::SkipDefinition::
if found_type_definition then
goto SkipTypeDefinition
Expand All @@ -244,7 +286,7 @@ function hover.on_hover(_, result, ctx, config)
end
log.debug { 'Hover: type definition location', type_def_suffix }
table.insert(actions, 1, string.format('%d. Go to type definition at ' .. type_def_suffix, #actions + 1))
table.insert(_state.commands, function()
local typedef_action = function()
-- We don't call vim.lsp.buf.typeDefinition() because the location params may have changed
local type_definition_ctx = vim.tbl_extend('force', ctx, {
method = 'textDocument/typeDefinition',
Expand All @@ -253,7 +295,10 @@ function hover.on_hover(_, result, ctx, config)
---Neovim 0.9 has a bug in the lua doc
---@diagnostic disable-next-line: param-type-mismatch
vim.lsp.handlers['textDocument/typeDefinition'](nil, type_definition_result, type_definition_ctx)
end)
close_hover()
end
table.insert(_state.commands, typedef_action)
create_plug_mapping('TypeDefinition', typedef_action, current_bufnr)
::SkipTypeDefinition::
end
for _, pos in ipairs(to_remove) do
Expand All @@ -275,38 +320,52 @@ function hover.on_hover(_, result, ctx, config)
focus_id = 'haskell-tools-hover',
close_events = { 'CursorMoved', 'BufHidden', 'InsertCharPre' },
}, config or {})
local bufnr, winnr = lsp_util.open_floating_preview(markdown_lines, 'markdown', config)
local hover_bufnr, winnr = lsp_util.open_floating_preview(markdown_lines, 'markdown', config)
if opts.stylize_markdown == false then
vim.bo[bufnr].filetype = 'markdown'
vim.bo[hover_bufnr].filetype = 'markdown'
end
if opts.auto_focus == true then
vim.api.nvim_set_current_win(winnr)
end

if _state.winnr ~= nil then
return bufnr, winnr
return hover_bufnr, winnr
end

_state.winnr = winnr
vim.keymap.set('n', '<Esc>', close_hover, { buffer = bufnr, noremap = true, silent = true })
vim.api.nvim_buf_attach(bufnr, false, {
vim.keymap.set('n', '<Esc>', close_hover, { buffer = hover_bufnr, noremap = true, silent = true })
vim.api.nvim_buf_attach(hover_bufnr, false, {
on_detach = function()
_state.winnr = nil
end,
})

if #_state.commands == 0 then
return bufnr, winnr
return hover_bufnr, winnr
end

vim.api.nvim_set_option_value('cursorline', true, { win = winnr })

-- run the command under the cursor
vim.keymap.set('n', '<CR>', function()
run_command()
end, { buffer = bufnr, noremap = true, silent = true })
local win = vim.api.nvim_get_current_win()
local line = vim.api.nvim_win_get_cursor(win)[1]
run_command(line)
end, { buffer = hover_bufnr, noremap = true, silent = true })

local function run_command_mapping()
run_command(math.max(vim.v.count, 1))
end
create_plug_mapping('', run_command_mapping, current_bufnr)
vim.api.nvim_create_autocmd('WinClosed', {
group = vim.api.nvim_create_augroup('HaskellHoverAction', { clear = true }),
pattern = tostring(winnr),
callback = function()
delete_plug_mappings(current_bufnr)
end,
})

return bufnr, winnr
return hover_bufnr, winnr
end

return hover