Regenerate nvim config
This commit is contained in:
@ -0,0 +1,49 @@
|
||||
---@diagnostic disable: deprecated, duplicate-doc-field, duplicate-doc-alias
|
||||
---@mod haskell-tools.compat Functions for backward compatibility with older Neovim versions
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local compat = {}
|
||||
|
||||
compat.joinpath = vim.fs.joinpath or function(...)
|
||||
return (table.concat({ ... }, '/'):gsub('//+', '/'))
|
||||
end
|
||||
|
||||
compat.get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
|
||||
|
||||
compat.uv = vim.uv or vim.loop
|
||||
|
||||
--- @class vim.SystemCompleted
|
||||
--- @field code integer
|
||||
--- @field signal integer
|
||||
--- @field stdout? string
|
||||
--- @field stderr? string
|
||||
|
||||
--- @alias lsp.Client vim.lsp.Client
|
||||
|
||||
compat.system = vim.system
|
||||
-- wrapper around vim.fn.system to give it a similar API to vim.system
|
||||
or function(cmd, _, on_exit)
|
||||
local output = vim.fn.system(cmd)
|
||||
local ok = vim.v.shell_error
|
||||
---@type vim.SystemCompleted
|
||||
local systemObj = {
|
||||
signal = 0,
|
||||
stdout = ok and (output or '') or nil,
|
||||
stderr = not ok and (output or '') or nil,
|
||||
code = vim.v.shell_error,
|
||||
}
|
||||
on_exit(systemObj)
|
||||
return systemObj
|
||||
end
|
||||
|
||||
---@type fun(tbl:table):table
|
||||
compat.tbl_flatten = vim.iter and function(tbl)
|
||||
return vim.iter(tbl):flatten(math.huge):totable()
|
||||
end or vim.tbl_flatten
|
||||
|
||||
return compat
|
||||
@ -0,0 +1,202 @@
|
||||
---@mod haskell-tools.config.check haskell-tools configuration check
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
---@class HtConfigCheck
|
||||
local HtConfigCheck = {}
|
||||
|
||||
---@param path string
|
||||
---@param msg string|nil
|
||||
---@return string
|
||||
local function mk_error_msg(path, msg)
|
||||
return msg and path .. '.' .. msg or path
|
||||
end
|
||||
|
||||
---@param path string The config path
|
||||
---@param tbl table The table to validate
|
||||
---@see vim.validate
|
||||
---@return boolean is_valid
|
||||
---@return string|nil error_message
|
||||
local function validate(path, tbl)
|
||||
local prefix = 'Invalid config: '
|
||||
local ok, err = pcall(vim.validate, tbl)
|
||||
return ok or false, prefix .. mk_error_msg(path, err)
|
||||
end
|
||||
|
||||
---Validates the config.
|
||||
---@param cfg HTConfig
|
||||
---@return boolean is_valid
|
||||
---@return string|nil error_message
|
||||
function HtConfigCheck.validate(cfg)
|
||||
local ok, err
|
||||
local hls = cfg.hls
|
||||
ok, err = validate('hls', {
|
||||
auto_attach = { hls.auto_attach, { 'boolean', 'function' } },
|
||||
capabilities = { hls.capabilities, 'table' },
|
||||
cmd = { hls.cmd, { 'table', 'function' } },
|
||||
debug = { hls.debug, 'boolean' },
|
||||
default_settings = { hls.default_settings, 'table' },
|
||||
on_attach = { hls.on_attach, 'function' },
|
||||
settings = { hls.settings, { 'function', 'table' } },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local tools = cfg.tools
|
||||
local codeLens = tools.codeLens
|
||||
ok, err = validate('tools.codeLens', {
|
||||
autoRefresh = { codeLens.autoRefresh, { 'boolean', 'function' } },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local definition = tools.definition
|
||||
ok, err = validate('tools.definition', {
|
||||
hoogle_signature_fallback = { definition.hoogle_signature_fallback, { 'boolean', 'function' } },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local hoogle = tools.hoogle
|
||||
local valid_modes = { 'auto', 'telescope-local', 'telescope-web', 'browser' }
|
||||
ok, err = validate('tools.hoogle', {
|
||||
mode = {
|
||||
hoogle.mode,
|
||||
function(mode)
|
||||
return vim.tbl_contains(valid_modes, mode)
|
||||
end,
|
||||
'one of ' .. vim.inspect(valid_modes),
|
||||
},
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local hover = tools.hover
|
||||
ok, err = validate('tools.hover', {
|
||||
auto_focus = { hover.auto_focus, 'boolean', true },
|
||||
border = { hover.border, 'table', true },
|
||||
enable = { hover.enable, { 'boolean', 'function' } },
|
||||
stylize_markdown = { hover.stylize_markdown, 'boolean' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local log = tools.log
|
||||
ok, err = validate('tools.log', {
|
||||
log = { log.level, { 'number', 'string' } },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local repl = tools.repl
|
||||
local valid_handlers = { 'builtin', 'toggleterm' }
|
||||
local valid_backends = { 'cabal', 'stack' }
|
||||
ok, err = validate('tools.repl', {
|
||||
auto_focus = { repl.auto_focus, 'boolean', true },
|
||||
builtin = { repl.builtin, 'table' },
|
||||
handler = {
|
||||
repl.handler,
|
||||
function(handler)
|
||||
return type(handler) == 'function' or vim.tbl_contains(valid_handlers, handler)
|
||||
end,
|
||||
'one of ' .. vim.inspect(valid_handlers),
|
||||
},
|
||||
prefer = {
|
||||
repl.prefer,
|
||||
function(backend)
|
||||
return type(backend) == 'function' or vim.tbl_contains(valid_backends, backend)
|
||||
end,
|
||||
'one of ' .. vim.inspect(valid_backends),
|
||||
},
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local builtin = repl.builtin
|
||||
ok, err = validate('tools.repl.builtin', {
|
||||
create_repl_window = { builtin.create_repl_window, 'function' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local tags = tools.tags
|
||||
ok, err = validate('tools.tags', {
|
||||
enable = { tags.enable, { 'boolean', 'function' } },
|
||||
package_events = { tags.package_events, 'table' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local dap = cfg.dap
|
||||
local valid_dap_log_levels = { 'Debug', 'Info', 'Warning', 'Error' }
|
||||
ok, err = validate('dap', {
|
||||
auto_discover = { dap.auto_discover, { 'boolean', 'table' } },
|
||||
cmd = { dap.cmd, { 'function', 'table' } },
|
||||
logFile = { dap.logFile, 'string' },
|
||||
logLevel = {
|
||||
dap.logLevel,
|
||||
function(level)
|
||||
return type(level) == 'string' and vim.tbl_contains(valid_dap_log_levels, level)
|
||||
end,
|
||||
'one of ' .. vim.inspect(valid_backends),
|
||||
},
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local auto_discover = dap.auto_discover
|
||||
if type(auto_discover) == 'table' then
|
||||
---@cast auto_discover AddDapConfigOpts
|
||||
ok, err = validate('dap.auto_discover', {
|
||||
autodetect = { auto_discover.autodetect, 'boolean' },
|
||||
settings_file_pattern = { auto_discover.settings_file_pattern, 'string' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Recursively check a table for unrecognized keys,
|
||||
---using a default table as a reference
|
||||
---@param tbl table
|
||||
---@param default_tbl table
|
||||
---@param ignored_keys string[]
|
||||
---@return string[]
|
||||
function HtConfigCheck.get_unrecognized_keys(tbl, default_tbl, ignored_keys)
|
||||
local unrecognized_keys = {}
|
||||
for k, _ in pairs(tbl) do
|
||||
unrecognized_keys[k] = true
|
||||
end
|
||||
for k, _ in pairs(default_tbl) do
|
||||
unrecognized_keys[k] = false
|
||||
end
|
||||
local ret = {}
|
||||
for k, _ in pairs(unrecognized_keys) do
|
||||
if unrecognized_keys[k] then
|
||||
ret[k] = k
|
||||
end
|
||||
if type(default_tbl[k]) == 'table' and tbl[k] then
|
||||
for _, subk in pairs(HtConfigCheck.get_unrecognized_keys(tbl[k], default_tbl[k], {})) do
|
||||
local key = k .. '.' .. subk
|
||||
ret[key] = key
|
||||
end
|
||||
end
|
||||
end
|
||||
for k, _ in pairs(ret) do
|
||||
for _, ignore in pairs(ignored_keys) do
|
||||
if vim.startswith(k, ignore) then
|
||||
ret[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return vim.tbl_keys(ret)
|
||||
end
|
||||
|
||||
return HtConfigCheck
|
||||
@ -0,0 +1,123 @@
|
||||
---@mod haskell-tools.config plugin configuration
|
||||
---
|
||||
---@brief [[
|
||||
---To configure haskell-tools.nvim, set the variable `vim.g.haskell_tools`,
|
||||
---which is a `HTOpts` table, in your neovim configuration.
|
||||
---
|
||||
---Example:
|
||||
--->
|
||||
------@type HTOpts
|
||||
---vim.g.haskell_tools = {
|
||||
--- ---@type ToolsOpts
|
||||
--- tools = {
|
||||
--- -- ...
|
||||
--- },
|
||||
--- ---@type HaskellLspClientOpts
|
||||
--- hls = {
|
||||
--- on_attach = function(client, bufnr)
|
||||
--- -- Set keybindings, etc. here.
|
||||
--- end,
|
||||
--- -- ...
|
||||
--- },
|
||||
--- ---@type HTDapOpts
|
||||
--- dap = {
|
||||
--- -- ...
|
||||
--- },
|
||||
--- }
|
||||
---<
|
||||
---
|
||||
---Note: `vim.g.haskell_tools` can also be a function that returns a 'HTOpts' table.
|
||||
---
|
||||
---@brief ]]
|
||||
|
||||
local config = {}
|
||||
|
||||
---@type (fun():HTOpts) | HTOpts | nil
|
||||
vim.g.haskell_tools = vim.g.haskell_tools
|
||||
|
||||
---@class HTOpts
|
||||
---@field tools? ToolsOpts haskell-tools module options.
|
||||
---@field hls? HaskellLspClientOpts haskell-language-server client options.
|
||||
---@field dap? HTDapOpts debug adapter config for nvim-dap.
|
||||
---@class ToolsOpts
|
||||
---@field codeLens? CodeLensOpts LSP codeLens options.
|
||||
---@field hoogle? HoogleOpts Hoogle type signature search options.
|
||||
---@field hover? HoverOpts LSP hover options.
|
||||
---@field definition? DefinitionOpts LSP go-to-definition options.
|
||||
---@field repl? ReplOpts GHCi repl options.
|
||||
---@field tags? FastTagsOpts fast-tags module options.
|
||||
---@field log? HTLogOpts haskell-tools logger options.
|
||||
|
||||
---@class CodeLensOpts
|
||||
---@field autoRefresh? (fun():boolean) | boolean (default: `true`) Whether to auto-refresh code-lenses.
|
||||
|
||||
---@class HoogleOpts
|
||||
---@field mode? HoogleMode Use a telescope with a local hoogle installation or a web backend, or use the browser for hoogle signature search?
|
||||
|
||||
---@alias HoogleMode 'auto' | 'telescope-local' | 'telescope-web' | 'browser'
|
||||
|
||||
---@class HoverOpts
|
||||
---@field enable? (fun():boolean) | boolean (default: `true`) Whether to enable haskell-tools hover.
|
||||
---@field border? string[][] The hover window's border. Set to `nil` to disable.
|
||||
---@field stylize_markdown? boolean (default: `false`) The builtin LSP client's default behaviour is to stylize markdown. Setting this option to false sets the file type to markdown and enables treesitter syntax highligting for Haskell snippets if nvim-treesitter is installed.
|
||||
---@field auto_focus? boolean (default: `false`) Whether to automatically switch to the hover window.
|
||||
|
||||
---@class DefinitionOpts
|
||||
---@field hoogle_signature_fallback? (fun():boolean) | boolean (default: `false`) Configure `vim.lsp.definition` to fall back to hoogle search (does not affect `vim.lsp.tagfunc`).
|
||||
|
||||
---@class ReplOpts
|
||||
---@field handler? (fun():ReplHandler) | ReplHandler `'builtin'`: Use the simple builtin repl. `'toggleterm'`: Use akinsho/toggleterm.nvim.
|
||||
---@field prefer? (fun():repl_backend) | repl_backend Prefer cabal or stack when both stack and cabal project files are present?
|
||||
---@field builtin? BuiltinReplOpts Configuration for the builtin repl.
|
||||
---@field auto_focus? boolean Whether to auto-focus the repl on toggle or send. If unset, the handler decides.
|
||||
|
||||
---@alias ReplHandler 'builtin' | 'toggleterm'
|
||||
---@alias repl_backend 'cabal' | 'stack'
|
||||
|
||||
---@class BuiltinReplOpts
|
||||
---@field create_repl_window? (fun(view:ReplView):fun(mk_repl_cmd:mk_repl_cmd_fun)) How to create the repl window. Should return a function that calls one of the `ReplView`'s functions.
|
||||
|
||||
---@class ReplView
|
||||
---@field create_repl_split? fun(opts:ReplViewOpts):mk_repl_cmd_fun Create the REPL in a horizontally split window.
|
||||
---@field create_repl_vsplit? fun(opts:ReplViewOpts):mk_repl_cmd_fun Create the REPL in a vertically split window.
|
||||
---@field create_repl_tabnew? fun(opts:ReplViewOpts):mk_repl_cmd_fun Create the REPL in a new tab.
|
||||
---@field create_repl_cur_win? fun(opts:ReplViewOpts):mk_repl_cmd_fun Create the REPL in the current window.
|
||||
|
||||
---@class ReplViewOpts
|
||||
---@field delete_buffer_on_exit? boolean Whether to delete the buffer when the Repl quits.
|
||||
---@field size? (fun():number) | number The size of the window or a function that determines it.
|
||||
|
||||
---@alias mk_repl_cmd_fun fun():(string[]|nil)
|
||||
|
||||
---@class FastTagsOpts
|
||||
---@field enable? boolean | (fun():boolean) Enabled by default if the `fast-tags` executable is found.
|
||||
---@field package_events? string[] `autocmd` Events to trigger package tag generation.
|
||||
|
||||
---@class HTLogOpts
|
||||
---@field level? number | string The log level.
|
||||
---@see vim.log.levels
|
||||
|
||||
---@class HaskellLspClientOpts
|
||||
---@field auto_attach? (fun():boolean) | boolean Whether to automatically attach the LSP client. Defaults to `true` if the haskell-language-server executable is found.
|
||||
---@field debug? boolean Whether to enable haskell-language-server debug logging.
|
||||
---@field on_attach? fun(client:number,bufnr:number,ht:HaskellTools) Callback that is invoked when the client attaches to a buffer.
|
||||
---@field cmd? (fun():string[]) | string[] The command to start haskell-language-server with.
|
||||
---@field capabilities? lsp.ClientCapabilities LSP client capabilities.
|
||||
---@field settings? (fun(project_root:string|nil):table) | table The haskell-language-server settings or a function that creates them. To view the default settings, run `haskell-language-server generate-default-config`.
|
||||
---@field default_settings? table The default haskell-language-server settings that will be used if no settings are specified or detected.
|
||||
---@field logfile? string The path to the haskell-language-server log file.
|
||||
|
||||
---@brief [[
|
||||
--- To print all options that are available for your haskell-language-server version, run `haskell-language-server-wrapper generate-default-config`
|
||||
---See: https://haskell-language-server.readthedocs.io/en/latest/configuration.html.
|
||||
---@brief ]]
|
||||
|
||||
---@class HTDapOpts
|
||||
---@field cmd? string[] The command to start the debug adapter server with.
|
||||
---@field logFile? string Log file path for detected configurations.
|
||||
---@field logLevel? HaskellDebugAdapterLogLevel The log level for detected configurations.
|
||||
---@field auto_discover? boolean | AddDapConfigOpts Set to `false` to disable auto-discovery of launch configurations. `true` uses the default configurations options`.
|
||||
|
||||
---@alias HaskellDebugAdapterLogLevel 'Debug' | 'Info' | 'Warning' | 'Error'
|
||||
|
||||
return config
|
||||
@ -0,0 +1,295 @@
|
||||
---@mod haskell-tools.config.internal
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- The internal configuration.
|
||||
--- Merges the default config with `vim.g.haskell_tools`.
|
||||
---@brief ]]
|
||||
|
||||
local deps = require('haskell-tools.deps')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@type HTConfig
|
||||
local HTConfig = {}
|
||||
|
||||
local ht_capabilities = vim.lsp.protocol.make_client_capabilities()
|
||||
local cmp_capabilities = deps.if_available('cmp_nvim_lsp', function(cmp_nvim_lsp)
|
||||
return cmp_nvim_lsp.default_capabilities()
|
||||
end, {})
|
||||
local selection_range_capabilities = deps.if_available('lsp-selection-range', function(lsp_selection_range)
|
||||
return lsp_selection_range.update_capabilities {}
|
||||
end, {})
|
||||
local folding_range_capabilities = deps.if_available('ufo', function(_)
|
||||
return {
|
||||
textDocument = {
|
||||
foldingRange = {
|
||||
dynamicRegistration = false,
|
||||
lineFoldingOnly = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
end, {})
|
||||
local capabilities = vim.tbl_deep_extend(
|
||||
'keep',
|
||||
ht_capabilities,
|
||||
cmp_capabilities,
|
||||
selection_range_capabilities,
|
||||
folding_range_capabilities
|
||||
)
|
||||
|
||||
---@class HTConfig haskell-tools.nvim plugin configuration.
|
||||
local HTDefaultConfig = {
|
||||
|
||||
---@class ToolsConfig haskell-tools module config.
|
||||
tools = {
|
||||
---@class CodeLensConfig LSP codeLens options.
|
||||
codeLens = {
|
||||
---@type boolean | (fun():boolean) (default: `true`) Whether to auto-refresh code-lenses.
|
||||
autoRefresh = true,
|
||||
},
|
||||
---@class HoogleConfig hoogle type signature search config.
|
||||
hoogle = {
|
||||
---@type HoogleMode Use a telescope with a local hoogle installation or a web backend, or use the browser for hoogle signature search?
|
||||
mode = 'auto',
|
||||
},
|
||||
---@class HoverConfig Enhanced LSP hover options.
|
||||
hover = {
|
||||
---@type boolean | (fun():boolean) (default: `true`) Whether to enable haskell-tools hover.
|
||||
enable = true,
|
||||
---@type string[][] | nil The hover window's border. Set to `nil` to disable.
|
||||
border = {
|
||||
{ '╭', 'FloatBorder' },
|
||||
{ '─', 'FloatBorder' },
|
||||
{ '╮', 'FloatBorder' },
|
||||
{ '│', 'FloatBorder' },
|
||||
{ '╯', 'FloatBorder' },
|
||||
{ '─', 'FloatBorder' },
|
||||
{ '╰', 'FloatBorder' },
|
||||
{ '│', 'FloatBorder' },
|
||||
},
|
||||
---@type boolean (default: `false`) The builtin LSP client's default behaviour is to stylize markdown. Setting this option to false sets the file type to markdown and enables treesitter syntax highligting for Haskell snippets if nvim-treesitter is installed.
|
||||
stylize_markdown = false,
|
||||
---@type boolean (default: `false`) Whether to automatically switch to the hover window.
|
||||
auto_focus = false,
|
||||
},
|
||||
---@class DefinitionConfig Enhanced LSP go-to-definition options.
|
||||
definition = {
|
||||
---@type boolean | (fun():boolean) (default: `false`) Configure `vim.lsp.definition` to fall back to hoogle search (does not affect `vim.lsp.tagfunc`).
|
||||
hoogle_signature_fallback = false,
|
||||
},
|
||||
---@class ReplConfig GHCi repl options.
|
||||
repl = {
|
||||
---@type ReplHandler | (fun():ReplHandler) `'builtin'`: Use the simple builtin repl. `'toggleterm'`: Use akinsho/toggleterm.nvim.
|
||||
handler = 'builtin',
|
||||
---@type repl_backend | (fun():repl_backend) Prefer cabal or stack when both stack and cabal project files are present?
|
||||
prefer = function()
|
||||
return vim.fn.executable('stack') == 1 and 'stack' or 'cabal'
|
||||
end,
|
||||
---@class BuiltinReplConfig Configuration for the builtin repl
|
||||
builtin = {
|
||||
---@type fun(view:ReplView):fun(mk_repl_cmd:mk_repl_cmd_fun) How to create the repl window. Should return a function that calls one of the `ReplView`'s functions.
|
||||
create_repl_window = function(view)
|
||||
return view.create_repl_split { size = vim.o.lines / 3 }
|
||||
end,
|
||||
},
|
||||
---@type boolean | nil Whether to auto-focus the repl on toggle or send. If unset, the handler decides.
|
||||
auto_focus = nil,
|
||||
},
|
||||
---@class FastTagsConfig fast-tags module options.
|
||||
tags = {
|
||||
---@type boolean | (fun():boolean) Enabled by default if the `fast-tags` executable is found.
|
||||
enable = function()
|
||||
return vim.fn.executable('fast-tags') == 1
|
||||
end,
|
||||
---@type string[] `autocmd` Events to trigger package tag generation.
|
||||
package_events = { 'BufWritePost' },
|
||||
},
|
||||
---@class HTLogConfig haskell-tools logger options.
|
||||
log = {
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
logfile = compat.joinpath(vim.fn.stdpath('log'), 'haskell-tools.log'),
|
||||
---@type number | string The log level.
|
||||
---@see vim.log.levels
|
||||
level = vim.log.levels.WARN,
|
||||
},
|
||||
},
|
||||
---@class HaskellLspClientConfig haskell-language-server client options.
|
||||
hls = {
|
||||
---@type boolean | (fun():boolean) Whether to automatically attach the LSP client. Defaults to `true` if the haskell-language-server executable is found.
|
||||
auto_attach = function()
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local cmd = Types.evaluate(HTConfig.hls.cmd)
|
||||
---@cast cmd string[]
|
||||
local hls_bin = cmd[1]
|
||||
return vim.fn.executable(hls_bin) == 1
|
||||
end,
|
||||
---@type boolean Whether to enable haskell-language-server debug logging.
|
||||
debug = false,
|
||||
---@type (fun(client:number,bufnr:number,ht:HaskellTools)) Callback that is invoked when the client attaches to a buffer.
|
||||
---@see vim.lsp.start
|
||||
on_attach = function(_, _, _) end,
|
||||
---@type string[] | (fun():string[]) The command to start haskell-language-server with.
|
||||
---@see vim.lsp.start
|
||||
cmd = function()
|
||||
-- Some distributions don't prorvide a hls wrapper.
|
||||
-- So we check if it exists and fall back to hls if it doesn't
|
||||
local hls_bin = 'haskell-language-server'
|
||||
local hls_wrapper_bin = hls_bin .. '-wrapper'
|
||||
local bin = vim.fn.executable(hls_wrapper_bin) == 1 and hls_wrapper_bin or hls_bin
|
||||
local cmd = { bin, '--lsp', '--logfile', HTConfig.hls.logfile }
|
||||
if HTConfig.hls.debug then
|
||||
table.insert(cmd, '--debug')
|
||||
end
|
||||
return cmd
|
||||
end,
|
||||
---@type lsp.ClientCapabilities | nil LSP client capabilities.
|
||||
---@see vim.lsp.protocol.make_client_capabilities
|
||||
---@see vim.lsp.start
|
||||
capabilities = capabilities,
|
||||
---@type table | (fun(project_root:string|nil):table) | nil The haskell-language-server settings or a function that creates them. To view the default settings, run `haskell-language-server generate-default-config`.
|
||||
settings = function(project_root)
|
||||
local ht = require('haskell-tools')
|
||||
return ht.lsp.load_hls_settings(project_root)
|
||||
end,
|
||||
---@type table The default haskell-language-server settings that will be used if no settings are specified or detected.
|
||||
default_settings = {
|
||||
haskell = {
|
||||
-- The formatting providers.
|
||||
formattingProvider = 'fourmolu',
|
||||
-- Maximum number of completions sent to the LSP client.
|
||||
maxCompletions = 40,
|
||||
-- Whether to typecheck the entire project on initial load.
|
||||
-- Could drive to bad performance in large projects, if set to true.
|
||||
checkProject = true,
|
||||
-- When to typecheck reverse dependencies of a file;
|
||||
-- one of NeverCheck, CheckOnSave (means dependent/parent modules will only be checked when you save),
|
||||
-- or AlwaysCheck (means re-typechecking them on every change).
|
||||
checkParents = 'CheckOnSave',
|
||||
plugin = {
|
||||
alternateNumberFormat = { globalOn = true },
|
||||
callHierarchy = { globalOn = true },
|
||||
changeTypeSignature = { globalOn = true },
|
||||
class = {
|
||||
codeActionsOn = true,
|
||||
codeLensOn = true,
|
||||
},
|
||||
eval = {
|
||||
globalOn = true,
|
||||
config = {
|
||||
diff = true,
|
||||
exception = true,
|
||||
},
|
||||
},
|
||||
excplicitFixity = { globalOn = true },
|
||||
gadt = { globalOn = true },
|
||||
['ghcide-code-actions-bindings'] = { globalOn = true },
|
||||
['ghcide-code-actions-fill-holes'] = { globalOn = true },
|
||||
['ghcide-code-actions-imports-exports'] = { globalOn = true },
|
||||
['ghcide-code-actions-type-signatures'] = { globalOn = true },
|
||||
['ghcide-completions'] = {
|
||||
globalOn = true,
|
||||
config = {
|
||||
autoExtendOn = true,
|
||||
snippetsOn = true,
|
||||
},
|
||||
},
|
||||
['ghcide-hover-and-symbols'] = {
|
||||
hoverOn = true,
|
||||
symbolsOn = true,
|
||||
},
|
||||
['ghcide-type-lenses'] = {
|
||||
globalOn = true,
|
||||
config = {
|
||||
mode = 'always',
|
||||
},
|
||||
},
|
||||
haddockComments = { globalOn = true },
|
||||
hlint = {
|
||||
codeActionsOn = true,
|
||||
diagnosticsOn = true,
|
||||
},
|
||||
importLens = {
|
||||
globalOn = true,
|
||||
codeActionsOn = true,
|
||||
codeLensOn = true,
|
||||
},
|
||||
moduleName = { globalOn = true },
|
||||
pragmas = {
|
||||
codeActionsOn = true,
|
||||
completionOn = true,
|
||||
},
|
||||
qualifyImportedNames = { globalOn = true },
|
||||
refineImports = {
|
||||
codeActionsOn = true,
|
||||
codeLensOn = true,
|
||||
},
|
||||
rename = {
|
||||
globalOn = true,
|
||||
config = { crossModule = true },
|
||||
},
|
||||
retrie = { globalOn = true },
|
||||
splice = { globalOn = true },
|
||||
tactics = {
|
||||
codeActionsOn = true,
|
||||
codeLensOn = true,
|
||||
config = {
|
||||
auto_gas = 4,
|
||||
hole_severity = nil,
|
||||
max_use_ctor_actions = 5,
|
||||
proofstate_styling = true,
|
||||
timeout_duration = 2,
|
||||
},
|
||||
hoverOn = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
---@type string The path to the haskell-language-server log file.
|
||||
logfile = vim.fn.tempname() .. '-haskell-language-server.log',
|
||||
},
|
||||
---@class HTDapConfig debug adapter config for nvim-dap.
|
||||
dap = {
|
||||
---@type string[] | (fun():string[]) The command to start the debug adapter server with.
|
||||
cmd = { 'haskell-debug-adapter' },
|
||||
---@type string Log file path for detected configurations.
|
||||
logFile = vim.fn.stdpath('data') .. '/haskell-dap.log',
|
||||
---@type HaskellDebugAdapterLogLevel The log level for detected configurations.
|
||||
logLevel = 'Warning',
|
||||
---@type boolean | AddDapConfigOpts Set to `false` to disable auto-discovery of launch configurations. `true` uses the default configurations options`.
|
||||
auto_discover = true,
|
||||
},
|
||||
debug_info = {
|
||||
---@type boolean
|
||||
was_g_haskell_tools_sourced = vim.g.haskell_tools ~= nil,
|
||||
},
|
||||
}
|
||||
|
||||
local haskell_tools = vim.g.haskell_tools or {}
|
||||
---@type HTOpts
|
||||
local opts = type(haskell_tools) == 'function' and haskell_tools() or haskell_tools
|
||||
|
||||
---@type HTConfig
|
||||
HTConfig = vim.tbl_deep_extend('force', {}, HTDefaultConfig, opts)
|
||||
local check = require('haskell-tools.config.check')
|
||||
local ok, err = check.validate(HTConfig)
|
||||
if not ok then
|
||||
vim.notify('haskell-tools: ' .. err, vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
local dont_warn = {
|
||||
'tools.repl.auto_focus',
|
||||
'hls.capabilities',
|
||||
'hls.settings',
|
||||
'hls.default_settings',
|
||||
}
|
||||
local unrecognized_keys = check.get_unrecognized_keys(opts, HTDefaultConfig, dont_warn)
|
||||
if #unrecognized_keys > 0 then
|
||||
vim.notify('unrecognized configs in vim.g.haskell_tools: ' .. vim.inspect(unrecognized_keys), vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
HTConfig.debug_info.unrecognized_keys = unrecognized_keys
|
||||
|
||||
return HTConfig
|
||||
@ -0,0 +1,156 @@
|
||||
---@mod haskell-tools.dap haskell-tools nvim-dap setup
|
||||
|
||||
local deps = require('haskell-tools.deps')
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@param root_dir string
|
||||
local function get_ghci_dap_cmd(root_dir)
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
if HtProjectHelpers.is_cabal_project(root_dir) then
|
||||
return 'cabal exec -- ghci-dap --interactive -i ${workspaceFolder}'
|
||||
else
|
||||
return 'stack ghci --test --no-load --no-build --main-is TARGET --ghci-options -fprint-evld-with-show'
|
||||
end
|
||||
end
|
||||
|
||||
---@param root_dir string
|
||||
---@param opts AddDapConfigOpts
|
||||
---@return HsDapLaunchConfiguration[]
|
||||
local function find_json_configurations(root_dir, opts)
|
||||
---@type HsDapLaunchConfiguration[]
|
||||
local configurations = {}
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local results = vim.fn.glob(compat.joinpath(root_dir, opts.settings_file_pattern), true, true)
|
||||
if #results == 0 then
|
||||
log.info(opts.settings_file_pattern .. ' not found in project root ' .. root_dir)
|
||||
else
|
||||
for _, launch_json in pairs(results) do
|
||||
local OS = require('haskell-tools.os')
|
||||
local content = OS.read_file(launch_json)
|
||||
local success, settings = pcall(vim.json.decode, content)
|
||||
if not success then
|
||||
local msg = 'Could not decode ' .. launch_json .. '.'
|
||||
log.warn { msg, error }
|
||||
elseif settings and settings.configurations and type(settings.configurations) == 'table' then
|
||||
configurations = vim.list_extend(configurations, settings.configurations)
|
||||
end
|
||||
end
|
||||
end
|
||||
return configurations
|
||||
end
|
||||
|
||||
---@param root_dir string
|
||||
---@return HsDapLaunchConfiguration[]
|
||||
local function detect_launch_configurations(root_dir)
|
||||
local launch_configurations = {}
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local dap_opts = HTConfig.dap
|
||||
---@param entry_point HsEntryPoint
|
||||
---@return HsDapLaunchConfiguration
|
||||
local function mk_launch_configuration(entry_point)
|
||||
---@class HsDapLaunchConfiguration
|
||||
local HsDapLaunchConfiguration = {
|
||||
type = 'ghc',
|
||||
request = 'launch',
|
||||
name = entry_point.package_name .. ':' .. entry_point.exe_name,
|
||||
workspace = '${workspaceFolder}',
|
||||
startup = compat.joinpath(entry_point.package_dir, entry_point.source_dir, entry_point.main),
|
||||
startupFunc = '', -- defaults to 'main' if not set
|
||||
startupArgs = '',
|
||||
stopOnEntry = false,
|
||||
mainArgs = '',
|
||||
logFile = dap_opts.logFile,
|
||||
logLevel = dap_opts.logLevel,
|
||||
ghciEnv = vim.empty_dict(),
|
||||
ghciPrompt = 'λ: ',
|
||||
ghciInitialPrompt = 'ghci> ',
|
||||
ghciCmd = get_ghci_dap_cmd(root_dir),
|
||||
forceInspect = false,
|
||||
}
|
||||
return HsDapLaunchConfiguration
|
||||
end
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
for _, entry_point in pairs(HtProjectHelpers.parse_project_entrypoints(root_dir)) do
|
||||
table.insert(launch_configurations, mk_launch_configuration(entry_point))
|
||||
end
|
||||
return launch_configurations
|
||||
end
|
||||
|
||||
---@type table<string, table>
|
||||
local _configuration_cache = {}
|
||||
|
||||
if not deps.has('dap') then
|
||||
---@type HsDapTools
|
||||
local NullHsDapTools = {
|
||||
discover_configurations = function(_) end,
|
||||
}
|
||||
return NullHsDapTools
|
||||
end
|
||||
|
||||
local dap = require('dap')
|
||||
|
||||
---@class HsDapTools
|
||||
local HsDapTools = {}
|
||||
|
||||
---@class AddDapConfigOpts
|
||||
local DefaultAutoDapConfigOpts = {
|
||||
---@type boolean Whether to automatically detect launch configurations for the project.
|
||||
autodetect = true,
|
||||
---@type string File name or pattern to search for. Defaults to 'launch.json'.
|
||||
settings_file_pattern = 'launch.json',
|
||||
}
|
||||
|
||||
---Discover nvim-dap launch configurations for haskell-debug-adapter.
|
||||
---@param bufnr number|nil The buffer number
|
||||
---@param opts AddDapConfigOpts|nil
|
||||
---@return nil
|
||||
HsDapTools.discover_configurations = function(bufnr, opts)
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local HTDapConfig = HTConfig.dap
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local dap_cmd = Types.evaluate(HTDapConfig.cmd) or {}
|
||||
if #dap_cmd == 0 or vim.fn.executable(dap_cmd[1]) ~= 1 then
|
||||
log.debug { 'DAP server executable not found.', dap_cmd }
|
||||
return
|
||||
end
|
||||
---@cast dap_cmd string[]
|
||||
dap.adapters.ghc = {
|
||||
type = 'executable',
|
||||
command = table.concat(dap_cmd, ' '),
|
||||
}
|
||||
bufnr = bufnr or 0 -- Default to current buffer
|
||||
opts = vim.tbl_deep_extend('force', {}, DefaultAutoDapConfigOpts, opts or {})
|
||||
local filename = vim.api.nvim_buf_get_name(bufnr)
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local project_root = HtProjectHelpers.match_project_root(filename)
|
||||
if not project_root then
|
||||
log.warn('dap: Unable to detect project root for file ' .. filename)
|
||||
return
|
||||
end
|
||||
if _configuration_cache[project_root] then
|
||||
log.debug('dap: Found cached configuration. Skipping.')
|
||||
return
|
||||
end
|
||||
local discovered_configurations = {}
|
||||
local json_configurations = find_json_configurations(project_root, opts)
|
||||
vim.list_extend(discovered_configurations, json_configurations)
|
||||
if opts.autodetect then
|
||||
local detected_configurations = detect_launch_configurations(project_root)
|
||||
vim.list_extend(discovered_configurations, detected_configurations)
|
||||
end
|
||||
_configuration_cache[project_root] = discovered_configurations
|
||||
---@type HsDapLaunchConfiguration[]
|
||||
local dap_configurations = dap.configurations.haskell or {}
|
||||
for _, cfg in ipairs(discovered_configurations) do
|
||||
for i, existing_config in pairs(dap_configurations) do
|
||||
if cfg.name == existing_config.name and cfg.startup == existing_config.startup then
|
||||
table.remove(dap_configurations, i)
|
||||
end
|
||||
end
|
||||
table.insert(dap_configurations, cfg)
|
||||
end
|
||||
dap.configurations.haskell = dap_configurations
|
||||
end
|
||||
|
||||
return HsDapTools
|
||||
@ -0,0 +1,33 @@
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class DapInternal
|
||||
local Dap = {}
|
||||
|
||||
---@param package_name string
|
||||
---@param exe_name string
|
||||
---@param package_dir string
|
||||
---@param mains string[]
|
||||
---@param source_dirs string[]
|
||||
---@return HsEntryPoint[] entry_points
|
||||
Dap.mk_entry_points = function(package_name, exe_name, package_dir, mains, source_dirs)
|
||||
---@type HsEntryPoint[]
|
||||
local entry_points = {}
|
||||
for _, source_dir in pairs(source_dirs) do
|
||||
for _, main in pairs(mains) do
|
||||
local filename = compat.joinpath(package_dir, source_dir, main)
|
||||
if vim.fn.filereadable(filename) == 1 then
|
||||
local entry_point = {
|
||||
package_name = package_name,
|
||||
exe_name = exe_name,
|
||||
main = main,
|
||||
source_dir = source_dir,
|
||||
package_dir = package_dir,
|
||||
}
|
||||
table.insert(entry_points, entry_point)
|
||||
end
|
||||
end
|
||||
end
|
||||
return entry_points
|
||||
end
|
||||
|
||||
return Dap
|
||||
@ -0,0 +1,80 @@
|
||||
---@mod haskell-tools.deps
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
---@class Deps
|
||||
local Deps = {}
|
||||
|
||||
---@param modname string The name of the module
|
||||
---@param on_available any|nil Callback. Can be a function that takes the module name as an argument or a value.
|
||||
---@param on_not_available any|nil Callback to execute if the module is not available. Can be a function or a value.
|
||||
---@return any result Return value of on_available or on_not_available
|
||||
function Deps.if_available(modname, on_available, on_not_available)
|
||||
local has_mod, mod = pcall(require, modname)
|
||||
if has_mod and type(on_available) == 'function' then
|
||||
return on_available(mod)
|
||||
elseif has_mod then
|
||||
return on_available
|
||||
end
|
||||
if not on_not_available then
|
||||
return nil
|
||||
end
|
||||
if type(on_not_available) == 'function' then
|
||||
return on_not_available()
|
||||
end
|
||||
return on_not_available
|
||||
end
|
||||
|
||||
---Require a module or fail
|
||||
---@param modname string
|
||||
---@param plugin_name string
|
||||
---@return unknown
|
||||
---@require
|
||||
function Deps.require_or_err(modname, plugin_name)
|
||||
return Deps.if_available(modname, function(mod)
|
||||
return mod
|
||||
end, function()
|
||||
error('haskell-tools: This plugin requires the ' .. plugin_name .. ' plugin.')
|
||||
end)
|
||||
end
|
||||
|
||||
---@param modname string The name of the module
|
||||
---@return boolean
|
||||
function Deps.has(modname)
|
||||
return Deps.if_available(modname, true, false)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Deps.has_telescope()
|
||||
return Deps.has('telescope')
|
||||
end
|
||||
|
||||
---@return unknown
|
||||
---@require
|
||||
function Deps.require_telescope(modname)
|
||||
return Deps.require_or_err(modname, 'nvim-telescope/telescope.nvim')
|
||||
end
|
||||
|
||||
---@return unknown
|
||||
---@require
|
||||
function Deps.require_toggleterm(modname)
|
||||
return Deps.require_or_err(modname, 'akinsho/toggleterm')
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Deps.has_toggleterm()
|
||||
return Deps.has('toggleterm')
|
||||
end
|
||||
|
||||
---@return unknown
|
||||
---@require
|
||||
function Deps.require_iron(modname)
|
||||
return Deps.require_or_err(modname, 'hkupty/iron.nvim')
|
||||
end
|
||||
|
||||
return Deps
|
||||
@ -0,0 +1,262 @@
|
||||
---@mod haskell-tools.health Health checks
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local health = {}
|
||||
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local deps = require('haskell-tools.deps')
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local h = vim.health or require('health')
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local start = h.start or h.report_start
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local ok = h.ok or h.report_ok
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local error = h.error or h.report_error
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local warn = h.warn or h.report_warn
|
||||
|
||||
---@class LuaDependency
|
||||
---@field module string The name of a module
|
||||
---@field optional fun():boolean Function that returns whether the dependency is optional
|
||||
---@field url string URL (markdown)
|
||||
---@field info string Additional information
|
||||
|
||||
---@type LuaDependency[]
|
||||
local lua_dependencies = {
|
||||
{
|
||||
module = 'telescope',
|
||||
optional = function()
|
||||
if not HTConfig then
|
||||
return true
|
||||
end
|
||||
local hoogle_mode = HTConfig.tools.hoogle.mode
|
||||
return hoogle_mode:match('telescope') == nil
|
||||
end,
|
||||
url = '[nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)',
|
||||
info = 'Required for hoogle search modes "telescope-local" and "telescope-web"',
|
||||
},
|
||||
}
|
||||
|
||||
---@class ExternalDependency
|
||||
---@field name string Name of the dependency
|
||||
---@field get_binaries fun():string[]Function that returns the binaries to check for
|
||||
---@field optional fun():boolean Function that returns whether the dependency is optional
|
||||
---@field url string URL (markdown)
|
||||
---@field info string Additional information
|
||||
---@field extra_checks function|nil Optional extra checks to perform if the dependency is installed
|
||||
|
||||
---@type ExternalDependency[]
|
||||
local external_dependencies = {
|
||||
{
|
||||
name = 'haskell-language-server',
|
||||
get_binaries = function()
|
||||
local default = { 'haskell-language-server-wrapper', 'haskell-language-server' }
|
||||
if not HTConfig then
|
||||
return default
|
||||
end
|
||||
local cmd = Types.evaluate(HTConfig.hls.cmd)
|
||||
if not cmd or #cmd == 0 then
|
||||
return default
|
||||
end
|
||||
return { cmd[1] }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[haskell-language-server](https://haskell-language-server.readthedocs.io)',
|
||||
info = 'Required by the LSP client.',
|
||||
},
|
||||
{
|
||||
name = 'hoogle',
|
||||
get_binaries = function()
|
||||
return { 'hoogle' }
|
||||
end,
|
||||
optional = function()
|
||||
if not HTConfig then
|
||||
return true
|
||||
end
|
||||
local hoogle_mode = HTConfig.tools.hoogle.mode
|
||||
return hoogle_mode ~= 'telescope-local'
|
||||
end,
|
||||
url = '[ndmitchell/hoogle](https://github.com/ndmitchell/hoogle)',
|
||||
info = [[
|
||||
Recommended for better Hoogle search performance.
|
||||
Without a local installation, the web API will be used by default.
|
||||
Required if the hoogle mode is set to "telescope-local".
|
||||
]],
|
||||
extra_checks = function()
|
||||
local handle, errmsg = io.popen('hoogle base')
|
||||
if handle then
|
||||
handle:close()
|
||||
end
|
||||
if errmsg then
|
||||
local hoogle_mode = HTConfig.tools.hoogle.mode
|
||||
if hoogle_mode and hoogle_mode == 'auto' or hoogle_mode == 'telescope-local' then
|
||||
error('hoogle: ' .. errmsg)
|
||||
else
|
||||
warn('hoogle: ' .. errmsg)
|
||||
end
|
||||
end
|
||||
end,
|
||||
},
|
||||
{
|
||||
name = 'fast-tags',
|
||||
get_binaries = function()
|
||||
return { 'fast-tags' }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[fast-tags](https://hackage.haskell.org/package/fast-tags)',
|
||||
info = 'Optional, for generating tags as a `tagfunc` fallback.',
|
||||
},
|
||||
{
|
||||
name = 'curl',
|
||||
get_binaries = function()
|
||||
return { 'curl' }
|
||||
end,
|
||||
optional = function()
|
||||
local hoogle_mode = HTConfig.tools.hoogle.mode
|
||||
return not hoogle_mode or hoogle_mode ~= 'telescope-web'
|
||||
end,
|
||||
url = '[curl](https://curl.se/)',
|
||||
info = 'Required for "telescope-web" hoogle seach mode.',
|
||||
},
|
||||
{
|
||||
name = 'haskell-debug-adapter',
|
||||
get_binaries = function()
|
||||
return { 'haskell-debug-adapter' }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[haskell-debug-adapter](https://github.com/phoityne/haskell-debug-adapter)',
|
||||
info = 'Optional, for `dap` support.',
|
||||
},
|
||||
{
|
||||
name = 'ghci-dap',
|
||||
get_binaries = function()
|
||||
return { 'ghci-dap' }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[ghci-dap](https://github.com/phoityne/ghci-dap)',
|
||||
info = 'Optional, for `dap` support.',
|
||||
},
|
||||
}
|
||||
|
||||
---@param dep LuaDependency
|
||||
local function check_lua_dependency(dep)
|
||||
if deps.has(dep.module) then
|
||||
ok(dep.url .. ' installed.')
|
||||
return
|
||||
end
|
||||
if dep.optional() then
|
||||
warn(('%s not installed. %s %s'):format(dep.module, dep.info, dep.url))
|
||||
else
|
||||
error(('Lua dependency %s not found: %s'):format(dep.module, dep.url))
|
||||
end
|
||||
end
|
||||
|
||||
---@param dep ExternalDependency
|
||||
---@return boolean is_installed
|
||||
---@return string|nil version
|
||||
local check_installed = function(dep)
|
||||
local binaries = dep.get_binaries()
|
||||
for _, binary in ipairs(binaries) do
|
||||
if vim.fn.executable(binary) == 1 then
|
||||
local handle = io.popen(binary .. ' --version')
|
||||
if handle then
|
||||
local binary_version, error_msg = handle:read('*a')
|
||||
handle:close()
|
||||
if error_msg then
|
||||
return true
|
||||
end
|
||||
return true, binary_version
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param dep ExternalDependency
|
||||
local function check_external_dependency(dep)
|
||||
local installed, mb_version = check_installed(dep)
|
||||
if installed then
|
||||
local mb_version_newline_idx = mb_version and mb_version:find('\n')
|
||||
local mb_version_len = mb_version and (mb_version_newline_idx and mb_version_newline_idx - 1 or mb_version:len())
|
||||
local version = mb_version and mb_version:sub(0, mb_version_len) or '(unknown version)'
|
||||
ok(('%s: found %s'):format(dep.name, version))
|
||||
if dep.extra_checks then
|
||||
dep.extra_checks()
|
||||
end
|
||||
return
|
||||
end
|
||||
if dep.optional() then
|
||||
warn(([[
|
||||
%s: not found.
|
||||
Install %s for extended capabilities.
|
||||
%s
|
||||
]]):format(dep.name, dep.url, dep.info))
|
||||
else
|
||||
error(([[
|
||||
%s: not found.
|
||||
haskell-tools.nvim requires %s.
|
||||
%s
|
||||
]]):format(dep.name, dep.url, dep.info))
|
||||
end
|
||||
end
|
||||
|
||||
local function check_config()
|
||||
start('Checking config')
|
||||
if vim.g.haskell_tools and not HTConfig.debug_info.was_g_haskell_tools_sourced then
|
||||
error('vim.g.haskell_tools is set, but was not sourced before haskell-tools.nvim was initialized.')
|
||||
return
|
||||
end
|
||||
local valid, err = require('haskell-tools.config.check').validate(HTConfig)
|
||||
if valid then
|
||||
ok('No errors found in config.')
|
||||
else
|
||||
error(err or '' .. vim.g.haskell_tools and '' or ' This looks like a plugin bug!')
|
||||
end
|
||||
local unrecognized_keys = HTConfig.debug_info.unrecognized_keys
|
||||
if #unrecognized_keys > 0 then
|
||||
warn('unrecognized configs in vim.g.haskell_tools: ' .. vim.inspect(unrecognized_keys))
|
||||
end
|
||||
end
|
||||
|
||||
local function check_for_conflicts()
|
||||
start('Checking for conflicting plugins')
|
||||
for _, autocmd in ipairs(vim.api.nvim_get_autocmds { event = 'FileType', pattern = 'haskell' }) do
|
||||
if autocmd.group_name and autocmd.group_name == 'lspconfig' and autocmd.desc and autocmd.desc:match(' hls ') then
|
||||
error('lspconfig.hls has been setup. This will likely lead to conflicts with the haskell-tools LSP client.')
|
||||
return
|
||||
end
|
||||
end
|
||||
ok('No conflicting plugins detected.')
|
||||
end
|
||||
|
||||
function health.check()
|
||||
start('Checking for Lua dependencies')
|
||||
for _, dep in ipairs(lua_dependencies) do
|
||||
check_lua_dependency(dep)
|
||||
end
|
||||
|
||||
start('Checking external dependencies')
|
||||
for _, dep in ipairs(external_dependencies) do
|
||||
check_external_dependency(dep)
|
||||
end
|
||||
check_config()
|
||||
check_for_conflicts()
|
||||
end
|
||||
|
||||
return health
|
||||
@ -0,0 +1,147 @@
|
||||
---@mod haskell-tools.hoogle.helpers
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- This module provides hoogle search capabilities for telescope.nvim,
|
||||
--- The telescope search is mostly inspired by telescope_hoogle by Luc Tielen,
|
||||
--- but has been redesigned for searching for individual terms.
|
||||
--- https://github.com/luc-tielen/telescope_hoogle
|
||||
---@brief ]]
|
||||
|
||||
local deps = require('haskell-tools.deps')
|
||||
local OS = require('haskell-tools.os')
|
||||
local actions = deps.require_telescope('telescope.actions')
|
||||
local actions_state = deps.require_telescope('telescope.actions.state')
|
||||
local entry_display = deps.require_telescope('telescope.pickers.entry_display')
|
||||
|
||||
---@class HoogleHelpers
|
||||
local HoogleHelpers = {}
|
||||
|
||||
---@param buf number the telescope buffebuffer numberr
|
||||
---@param map fun(mode:string,keys:string,action:function) callback for creating telescope keymaps
|
||||
---@return boolean
|
||||
function HoogleHelpers.hoogle_attach_mappings(buf, map)
|
||||
actions.select_default:replace(function()
|
||||
-- Copy type signature to clipboard
|
||||
local entry = actions_state.get_selected_entry()
|
||||
local reg = vim.o.clipboard == 'unnamedplus' and '+' or '"'
|
||||
vim.fn.setreg(reg, entry.type_sig)
|
||||
actions.close(buf)
|
||||
end)
|
||||
map('i', '<C-b>', function()
|
||||
-- Open in browser
|
||||
local entry = actions_state.get_selected_entry()
|
||||
OS.open_browser(entry.url)
|
||||
end)
|
||||
map('i', '<C-r>', function()
|
||||
-- Replace word under cursor
|
||||
local entry = actions_state.get_selected_entry()
|
||||
local func_name = entry.type_sig:match('([^%s]*)%s::')
|
||||
if not func_name then
|
||||
vim.notify('Hoogle (replace): Not a function.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
actions.close(buf)
|
||||
vim.api.nvim_input('ciw' .. func_name .. '<ESC>')
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
---Format an html string to be displayed by Neovim
|
||||
---@param html string
|
||||
---@return string nvim_str
|
||||
local function format_html(html)
|
||||
return html and html:gsub('<', '<'):gsub('>', '>'):gsub('&', '&') or ''
|
||||
end
|
||||
|
||||
---@class TelescopeHoogleEntry
|
||||
---@field value string
|
||||
---@field valid boolean
|
||||
---@field type_sig string The entry's type signature
|
||||
---@field module_name string The name of the module that contains the entry
|
||||
---@field url string|nil The entry's Hackage URL
|
||||
---@field docs string|nil The Hoogle entry's documentation
|
||||
---@field display fun(TelescopeHoogleEntry):TelescopeDisplay
|
||||
---@field ordinal string
|
||||
---@field preview_command fun(TelescopeHoogleEntry, number):nil
|
||||
|
||||
---Show a preview in the Telescope previewer
|
||||
---@param entry TelescopeHoogleEntry
|
||||
---@param buf number the Telescope preview buffer
|
||||
local function show_preview(entry, buf)
|
||||
local docs = format_html(entry.docs)
|
||||
local lines = vim.split(docs, '\n')
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, true, lines)
|
||||
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
local win = vim.fn.win_findbuf(buf)[1]
|
||||
vim.wo[win].conceallevel = 2
|
||||
vim.wo[win].wrap = true
|
||||
vim.wo[win].linebreak = true
|
||||
vim.bo[buf].textwidth = 80
|
||||
end)
|
||||
end
|
||||
|
||||
---@class TelescopeDisplay
|
||||
|
||||
---@param entry TelescopeHoogleEntry
|
||||
---@return TelescopeDisplay
|
||||
local function make_display(entry)
|
||||
local module = entry.module_name
|
||||
|
||||
local displayer = entry_display.create {
|
||||
separator = '',
|
||||
items = {
|
||||
{ width = module and #module + 1 or 0 },
|
||||
{ remaining = true },
|
||||
},
|
||||
}
|
||||
return displayer { { module, 'Structure' }, { entry.type_sig, 'Type' } }
|
||||
end
|
||||
|
||||
---@param hoogle_item string
|
||||
---@return string type_signature
|
||||
local function get_type_sig(hoogle_item)
|
||||
local name = hoogle_item:match('<span class=name><s0>(.*)</s0></span>')
|
||||
local sig = hoogle_item:match(':: (.*)')
|
||||
if name and sig then
|
||||
local name_with_type = (name .. ' :: ' .. format_html(sig)):gsub('%s+', ' ') -- trim duplicate whitespace
|
||||
return name_with_type
|
||||
end
|
||||
return hoogle_item
|
||||
end
|
||||
|
||||
---@class HoogleData
|
||||
---@field module HoogleModule|nil
|
||||
---@field item string|nil
|
||||
---@field url string|nil
|
||||
---@field docs string|nil
|
||||
|
||||
---@class HoogleModule
|
||||
---@field name string
|
||||
|
||||
---@param data HoogleData
|
||||
---@return TelescopeHoogleEntry|nil
|
||||
function HoogleHelpers.mk_hoogle_entry(data)
|
||||
local module_name = (data.module or {}).name
|
||||
local type_sig = data.item and get_type_sig(data.item) or ''
|
||||
if not module_name or not type_sig then
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
value = data,
|
||||
valid = true,
|
||||
type_sig = type_sig,
|
||||
module_name = module_name,
|
||||
url = data.url,
|
||||
docs = data.docs,
|
||||
display = make_display,
|
||||
ordinal = data.item .. data.url,
|
||||
preview_command = show_preview,
|
||||
}
|
||||
end
|
||||
|
||||
return HoogleHelpers
|
||||
@ -0,0 +1,114 @@
|
||||
---@mod haskell-tools.hoogle haskell-tools Hoogle search
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local lsp_util = vim.lsp.util
|
||||
|
||||
---@type fun(sig_or_func_name:string, options:table|nil):nil
|
||||
local handler
|
||||
|
||||
---@param options table
|
||||
---@return fun(err: lsp.ResponseError|nil, result: any, context: lsp.HandlerContext, config: table|nil)
|
||||
local function mk_lsp_hoogle_signature_handler(options)
|
||||
return function(_, result, _, _)
|
||||
if not (result and result.contents) then
|
||||
vim.notify('hoogle: No information available')
|
||||
return
|
||||
end
|
||||
local func_name = vim.fn.expand('<cword>')
|
||||
---@cast func_name string
|
||||
local HtParser = require('haskell-tools.parser')
|
||||
local signature_or_func_name = HtParser.try_get_signatures_from_markdown(func_name, result.contents.value)
|
||||
or func_name
|
||||
log.debug { 'Hoogle LSP signature search', signature_or_func_name }
|
||||
if signature_or_func_name ~= '' then
|
||||
handler(signature_or_func_name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param options table
|
||||
local function lsp_hoogle_signature(options)
|
||||
local params = lsp_util.make_position_params()
|
||||
return vim.lsp.buf_request(0, 'textDocument/hover', params, mk_lsp_hoogle_signature_handler(options))
|
||||
end
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local opts = HTConfig.tools.hoogle
|
||||
local hoogle_web = require('haskell-tools.hoogle.web')
|
||||
local hoogle_local = require('haskell-tools.hoogle.local')
|
||||
|
||||
---@return nil
|
||||
local function set_web_handler()
|
||||
handler = hoogle_web.telescope_search
|
||||
log.debug('handler = telescope-web')
|
||||
end
|
||||
|
||||
---@return nil
|
||||
local function set_local_handler()
|
||||
handler = hoogle_local.telescope_search
|
||||
log.debug('handler = telescope-local')
|
||||
end
|
||||
|
||||
---@return nil
|
||||
local function set_browser_handler()
|
||||
handler = hoogle_web.browser_search
|
||||
log.debug('handler = browser')
|
||||
end
|
||||
|
||||
if opts.mode == 'telescope-web' then
|
||||
set_web_handler()
|
||||
elseif opts.mode == 'telescope-local' then
|
||||
if not hoogle_local.has_hoogle() then
|
||||
local msg = 'handler set to "telescope-local" but no hoogle executable found.'
|
||||
log.warn(msg)
|
||||
vim.notify('haskell-tools.hoogle: ' .. msg, vim.log.levels.WARN)
|
||||
set_web_handler()
|
||||
return
|
||||
end
|
||||
local deps = require('haskell-tools.deps')
|
||||
if not deps.has_telescope() then
|
||||
local msg = 'handler set to "telescope-local" but telescope.nvim is not installed.'
|
||||
log.warn(msg)
|
||||
vim.notify('haskell-tools.hoogle: ' .. msg, vim.log.levels.WARN)
|
||||
set_web_handler()
|
||||
return
|
||||
end
|
||||
set_local_handler()
|
||||
elseif opts.mode == 'browser' then
|
||||
set_browser_handler()
|
||||
elseif opts.mode == 'auto' then
|
||||
local deps = require('haskell-tools.deps')
|
||||
if not deps.has_telescope() then
|
||||
set_browser_handler()
|
||||
elseif hoogle_local.has_hoogle() then
|
||||
set_local_handler()
|
||||
else
|
||||
set_web_handler()
|
||||
end
|
||||
end
|
||||
|
||||
---@class HoogleTools
|
||||
local HoogleTools = {}
|
||||
|
||||
---@param options table<string,any>|nil Includes the `search_term` and options to pass to the telescope picker (if available)
|
||||
---@return nil
|
||||
HoogleTools.hoogle_signature = function(options)
|
||||
options = options or {}
|
||||
log.debug { 'Hoogle signature search options', options }
|
||||
if options.search_term then
|
||||
handler(options.search_term)
|
||||
return
|
||||
end
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
local clients = LspHelpers.get_clients { bufnr = vim.api.nvim_get_current_buf() }
|
||||
if #clients > 0 then
|
||||
lsp_hoogle_signature(options)
|
||||
else
|
||||
log.debug('Hoogle signature search: No clients attached. Falling back to <cword>.')
|
||||
local cword = vim.fn.expand('<cword>')
|
||||
---@cast cword string
|
||||
handler(cword, options)
|
||||
end
|
||||
end
|
||||
|
||||
return HoogleTools
|
||||
@ -0,0 +1,102 @@
|
||||
---@mod haskell-tools.hoogle.local
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local deps = require('haskell-tools.deps')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class LocalHoogleHandler
|
||||
local HoogleLocal = {}
|
||||
|
||||
---@return boolean has_hoogle `true` if the `hoogle` executable exists
|
||||
function HoogleLocal.has_hoogle()
|
||||
return vim.fn.executable('hoogle') == 1
|
||||
end
|
||||
|
||||
if not HoogleLocal.has_hoogle() then
|
||||
return HoogleLocal
|
||||
end
|
||||
|
||||
if not deps.has_telescope() then
|
||||
return HoogleLocal
|
||||
end
|
||||
|
||||
---@class LocalHoogleOpts
|
||||
---@field entry_maker function|nil telescope entry maker
|
||||
---@field count number|nil number of results to display
|
||||
|
||||
---Construct the hoogle cli arguments
|
||||
---@param search_term string The Hoogle search term
|
||||
---@param opts LocalHoogleOpts
|
||||
---@return string[] hoogle_args
|
||||
local function mk_hoogle_args(search_term, opts)
|
||||
local count = opts.count or 50
|
||||
local args = compat.tbl_flatten { '--json', '--count=' .. count, search_term }
|
||||
log.debug { 'Hoogle local args', args }
|
||||
return args
|
||||
end
|
||||
|
||||
local pickers = deps.require_telescope('telescope.pickers')
|
||||
local finders = deps.require_telescope('telescope.finders')
|
||||
local previewers = deps.require_telescope('telescope.previewers')
|
||||
local HoogleHelpers = require('haskell-tools.hoogle.helpers')
|
||||
|
||||
---@param search_term string The Hoogle search term
|
||||
---@param opts LocalHoogleOpts|nil
|
||||
---@return nil
|
||||
function HoogleLocal.telescope_search(search_term, opts)
|
||||
opts = opts or {}
|
||||
opts.entry_maker = opts.entry_maker or HoogleHelpers.mk_hoogle_entry
|
||||
local config = deps.require_telescope('telescope.config').values
|
||||
if not config then
|
||||
local msg = 'telescope.nvim has not been setup.'
|
||||
log.error(msg)
|
||||
vim.notify_once('haskell-tools.hoogle: ' .. msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local cmd = vim.list_extend({ 'hoogle' }, mk_hoogle_args(search_term, opts))
|
||||
compat.system(
|
||||
cmd,
|
||||
nil,
|
||||
vim.schedule_wrap(function(result)
|
||||
---@cast result vim.SystemCompleted
|
||||
local output = result.stdout
|
||||
if result.code ~= 0 or output == nil then
|
||||
local err_msg = 'haskell-tools: hoogle search failed. Exit code: ' .. result.code
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local success, results = pcall(vim.json.decode, output)
|
||||
if not success then
|
||||
log.error { 'Hoogle: Could not process result.', output }
|
||||
vim.notify('Hoogle: Could not process result - ' .. vim.inspect(output), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if #results < 1 or output == 'No results found' then
|
||||
vim.notify('Hoogle: No results found.', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
pickers
|
||||
.new(opts, {
|
||||
prompt_title = 'Hoogle: ' .. search_term,
|
||||
sorter = config.generic_sorter(opts),
|
||||
finder = finders.new_table {
|
||||
results = results,
|
||||
entry_maker = HoogleHelpers.mk_hoogle_entry,
|
||||
},
|
||||
previewer = previewers.display_content.new(opts),
|
||||
attach_mappings = HoogleHelpers.hoogle_attach_mappings,
|
||||
})
|
||||
:find()
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
return HoogleLocal
|
||||
@ -0,0 +1,129 @@
|
||||
---@mod haskell-tools.hoogle.web
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local deps = require('haskell-tools.deps')
|
||||
local OS = require('haskell-tools.os')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class WebHoogleHandler
|
||||
local WebHoogleHandler = {}
|
||||
|
||||
---@param c string A single character
|
||||
---@return string The hex representation
|
||||
local char_to_hex = function(c)
|
||||
return string.format('%%%02X', c:byte())
|
||||
end
|
||||
|
||||
---Encode a URL so it can be opened in a browser
|
||||
---@param url string
|
||||
---@return string encoded_url
|
||||
local function urlencode(url)
|
||||
url = url:gsub('\n', '\r\n')
|
||||
url = url:gsub('([^%w ])', char_to_hex)
|
||||
url = url:gsub(' ', '+')
|
||||
return url
|
||||
end
|
||||
|
||||
---@class TelescopeHoogleWebOpts
|
||||
---@field hoogle HoogleWebSearchOpts|nil
|
||||
|
||||
---@class HoogleWebSearchOpts
|
||||
---@field scope string|nil The scope of the search
|
||||
---@field json boolean|nil Whather to request JSON enocded results
|
||||
|
||||
---Build a Hoogle request URL
|
||||
---@param search_term string
|
||||
---@param opts TelescopeHoogleWebOpts
|
||||
local function mk_hoogle_request(search_term, opts)
|
||||
local hoogle_opts = opts.hoogle or {}
|
||||
local scope_param = hoogle_opts.scope and '&scope=' .. hoogle_opts.scope or ''
|
||||
local hoogle_request = 'https://hoogle.haskell.org/?hoogle='
|
||||
.. urlencode(search_term)
|
||||
.. scope_param
|
||||
.. (hoogle_opts.json and '&mode=json' or '')
|
||||
log.debug { 'Hoogle web request', hoogle_request }
|
||||
return hoogle_request
|
||||
end
|
||||
|
||||
if deps.has_telescope() then
|
||||
local pickers = deps.require_telescope('telescope.pickers')
|
||||
local finders = deps.require_telescope('telescope.finders')
|
||||
local previewers = deps.require_telescope('telescope.previewers')
|
||||
local HoogleHelpers = require('haskell-tools.hoogle.helpers')
|
||||
|
||||
---@param search_term string
|
||||
---@param opts TelescopeHoogleWebOpts|nil
|
||||
---@return nil
|
||||
function WebHoogleHandler.telescope_search(search_term, opts)
|
||||
local config = deps.require_telescope('telescope.config').values
|
||||
if not config then
|
||||
local msg = 'telescope.nvim has not been setup. Falling back to browser search.'
|
||||
log.warn(msg)
|
||||
vim.notify_once('haskell-tools.hoogle: ' .. msg, vim.log.levels.WARN)
|
||||
WebHoogleHandler.browser_search(search_term, opts)
|
||||
return
|
||||
end
|
||||
if vim.fn.executable('curl') == 0 then
|
||||
log.error('curl executable not found.')
|
||||
vim.notify("haskell-tools.hoogle-web: 'curl' executable not found! Aborting.", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
opts = opts or {}
|
||||
opts.hoogle = opts.hoogle or {}
|
||||
opts.hoogle.json = true
|
||||
local url = mk_hoogle_request(search_term, opts)
|
||||
local curl_command = { 'curl', '--silent', url, '-H', 'Accept: application/json' }
|
||||
log.debug(curl_command)
|
||||
compat.system(curl_command, nil, function(result)
|
||||
---@cast result vim.SystemCompleted
|
||||
log.debug { 'Hoogle web response', result }
|
||||
local response = result.stdout
|
||||
if result.code ~= 0 or response == nil then
|
||||
vim.notify('hoogle web: ' .. (result.stderr or 'error calling curl'), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local ok, results = pcall(vim.json.decode, response)
|
||||
vim.schedule(function()
|
||||
if not ok then
|
||||
log.error { 'Hoogle web response (invalid JSON)', curl_command, 'result: ' .. result }
|
||||
vim.notify(
|
||||
"haskell-tools.hoogle: Received invalid JSON from curl. Likely due to a failed request. See ':HtLog' for details'",
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
pickers
|
||||
.new(opts, {
|
||||
prompt_title = 'Hoogle: ' .. search_term,
|
||||
finder = finders.new_table {
|
||||
results = results,
|
||||
entry_maker = HoogleHelpers.mk_hoogle_entry,
|
||||
},
|
||||
sorter = config.generic_sorter(opts),
|
||||
previewer = previewers.display_content.new(opts),
|
||||
attach_mappings = HoogleHelpers.hoogle_attach_mappings,
|
||||
})
|
||||
:find()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param search_term string
|
||||
---@param opts TelescopeHoogleWebOpts|nil
|
||||
---@return nil
|
||||
function WebHoogleHandler.browser_search(search_term, opts)
|
||||
opts = vim.tbl_deep_extend('keep', opts or {}, {
|
||||
hoogle = { json = false },
|
||||
})
|
||||
OS.open_browser(mk_hoogle_request(search_term, opts))
|
||||
end
|
||||
|
||||
return WebHoogleHandler
|
||||
@ -0,0 +1,58 @@
|
||||
---@toc haskell-tools.contents
|
||||
|
||||
---@mod intro Introduction
|
||||
---@brief [[
|
||||
---This plugin automatically configures the `haskell-language-server` builtin LSP client
|
||||
---and integrates with other haskell tools.
|
||||
---@brief ]]
|
||||
---
|
||||
---@brief [[
|
||||
---WARNING:
|
||||
---Do not call the `lspconfig.hls` setup or set up the lsp manually,
|
||||
---as doing so may cause conflicts.
|
||||
---@brief ]]
|
||||
---
|
||||
---@brief [[
|
||||
---NOTE: This plugin is a filetype plugin.
|
||||
---There is no need to call a `setup` function.
|
||||
---@brief ]]
|
||||
|
||||
---@mod haskell-tools The haskell-tools module
|
||||
|
||||
---@brief [[
|
||||
---Entry-point into this plugin's public API.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
---@param module string
|
||||
---@return table
|
||||
local function lazy_require(module)
|
||||
return setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
return require(module)[key]
|
||||
end,
|
||||
__newindex = function(_, key, value)
|
||||
require(module)[key] = value
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@class HaskellTools
|
||||
local HaskellTools = {
|
||||
---@type HlsTools
|
||||
lsp = lazy_require('haskell-tools.lsp'),
|
||||
---@type HoogleTools
|
||||
hoogle = lazy_require('haskell-tools.hoogle'),
|
||||
---@type HsReplTools
|
||||
repl = lazy_require('haskell-tools.repl'),
|
||||
---@type HsProjectTools
|
||||
project = lazy_require('haskell-tools.project'),
|
||||
---@type FastTagsTools
|
||||
tags = lazy_require('haskell-tools.tags'),
|
||||
---@type HsDapTools
|
||||
dap = lazy_require('haskell-tools.dap'),
|
||||
---@type HaskellToolsLog
|
||||
log = lazy_require('haskell-tools.log'),
|
||||
}
|
||||
|
||||
return HaskellTools
|
||||
@ -0,0 +1,79 @@
|
||||
---@mod haskell-tools.internal
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- The internal API for use by this plugin's ftplugins
|
||||
---@brief ]]
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
|
||||
---@class InternalApi
|
||||
local InternalApi = {}
|
||||
|
||||
---@return boolean tf Is LSP supported for the current buffer?
|
||||
local function buf_is_lsp_supported()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
if not HtProjectHelpers.is_cabal_file(bufnr) then
|
||||
return true
|
||||
end
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
return LspHelpers.is_hls_version_with_cabal_support()
|
||||
end
|
||||
|
||||
---Starts or attaches an LSP client to the current buffer and sets up the plugin if necessary.
|
||||
---
|
||||
---@see haskell-tools.config for configuration options.
|
||||
---@see ftplugin
|
||||
---@see base-directories
|
||||
---@usage [[
|
||||
----- In your neovim configuration, set:
|
||||
---vim.g.haskell_tools = {
|
||||
--- tools = {
|
||||
--- -- ...
|
||||
--- },
|
||||
--- hls = {
|
||||
--- on_attach = function(client, bufnr)
|
||||
--- -- Set keybindings, etc. here.
|
||||
--- end,
|
||||
--- -- ...
|
||||
--- },
|
||||
--- }
|
||||
----- In `~/.config/nvim/ftplugin/after/<filetype>.lua`, call
|
||||
---local ht = require('haskell-tools')
|
||||
---ht.start_or_attach()
|
||||
---@usage ]]
|
||||
local function start_or_attach()
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local HaskellTools = require('haskell-tools')
|
||||
if Types.evaluate(HTConfig.hls.auto_attach) and buf_is_lsp_supported() then
|
||||
HaskellTools.lsp.start()
|
||||
end
|
||||
if Types.evaluate(HTConfig.tools.tags.enable) then
|
||||
HaskellTools.tags.generate_project_tags(nil, { refresh = false })
|
||||
end
|
||||
end
|
||||
|
||||
---Auto-discover nvim-dap launch configurations (if auto-discovery is enabled)
|
||||
local function dap_discover()
|
||||
local auto_discover = HTConfig.dap.auto_discover
|
||||
if not auto_discover then
|
||||
return
|
||||
elseif type(auto_discover) == 'boolean' then
|
||||
return require('haskell-tools').dap.discover_configurations()
|
||||
end
|
||||
---@cast auto_discover AddDapConfigOpts
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
require('haskell-tools').dap.discover_configurations(bufnr, auto_discover)
|
||||
end
|
||||
|
||||
---ftplugin implementation
|
||||
function InternalApi.ftplugin()
|
||||
start_or_attach()
|
||||
dap_discover()
|
||||
end
|
||||
|
||||
return InternalApi
|
||||
@ -0,0 +1,76 @@
|
||||
---@mod haskell-tools.log haskell-tools Logging
|
||||
---
|
||||
---@brief [[
|
||||
--- The following commands are available:
|
||||
---
|
||||
--- * `:HtLog` - Open the haskell-tools.nvim log file.
|
||||
--- * `:HlsLog` - Open the haskell-language-server log file.
|
||||
--- * `:HtSetLogLevel` - Set the haskell-tools.nvim and LSP client log level.
|
||||
---@brief ]]
|
||||
|
||||
---@class HaskellToolsLog
|
||||
local HaskellToolsLog = {}
|
||||
|
||||
---Get the haskell-language-server log file
|
||||
---@return string filepath
|
||||
function HaskellToolsLog.get_hls_logfile()
|
||||
return require('haskell-tools.log.internal').get_hls_logfile()
|
||||
end
|
||||
|
||||
---Get the haskell-tools.nvim log file path.
|
||||
---@return string filepath
|
||||
function HaskellToolsLog.get_logfile()
|
||||
return require('haskell-tools.log.internal').get_logfile()
|
||||
end
|
||||
|
||||
---Open the haskell-language-server log file
|
||||
---@return nil
|
||||
function HaskellToolsLog.nvim_open_hls_logfile()
|
||||
return require('haskell-tools.log.internal').nvim_open_hls_logfile()
|
||||
end
|
||||
|
||||
---Open the haskell-tools.nvim log file.
|
||||
---@return nil
|
||||
function HaskellToolsLog.nvim_open_logfile()
|
||||
return require('haskell-tools.log.internal').nvim_open_logfile()
|
||||
end
|
||||
|
||||
---Set the haskell-tools.nvim and LSP client log level
|
||||
---@param level (string|integer) The log level
|
||||
---@return nil
|
||||
---@see vim.log.levels
|
||||
function HaskellToolsLog.set_level(level)
|
||||
return require('haskell-tools.log.internal').set_level(level)
|
||||
end
|
||||
|
||||
local commands = {
|
||||
{
|
||||
'HtLog',
|
||||
function()
|
||||
HaskellToolsLog.nvim_open_logfile()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HlsLog',
|
||||
function()
|
||||
HaskellToolsLog.nvim_open_hls_logfile()
|
||||
end,
|
||||
{ nargs = 1 },
|
||||
},
|
||||
{
|
||||
'HtSetLogLevel',
|
||||
function(tbl)
|
||||
local level = vim.fn.expand(tbl.args)
|
||||
---@cast level string
|
||||
HaskellToolsLog.set_level(tonumber(level) or level)
|
||||
end,
|
||||
{ nargs = 1 },
|
||||
},
|
||||
}
|
||||
|
||||
for _, command in ipairs(commands) do
|
||||
vim.api.nvim_create_user_command(unpack(command))
|
||||
end
|
||||
|
||||
return HaskellToolsLog
|
||||
@ -0,0 +1,148 @@
|
||||
---@mod haskell-tools.log.internal haskell-tools Logging (internal)
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- The internal API for use by this plugin's ftplugins
|
||||
---@brief ]]
|
||||
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class HaskellToolsLogInternal
|
||||
local HaskellToolsLogInternal = {
|
||||
-- NOTE: These functions are initialised as empty for type checking purposes
|
||||
-- and implemented later.
|
||||
trace = function(_) end,
|
||||
debug = function(_) end,
|
||||
info = function(_) end,
|
||||
warn = function(_) end,
|
||||
error = function(_) end,
|
||||
}
|
||||
|
||||
local LARGE = 1e9
|
||||
|
||||
local log_date_format = '%F %H:%M:%S'
|
||||
|
||||
local function format_log(arg)
|
||||
return vim.inspect(arg, { newline = '' })
|
||||
end
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
|
||||
local logfilename = HTConfig.tools.log.logfile
|
||||
|
||||
---Get the haskell-tools.nvim log file path.
|
||||
---@return string filepath
|
||||
function HaskellToolsLogInternal.get_logfile()
|
||||
return logfilename
|
||||
end
|
||||
|
||||
---Open the haskell-tools.nvim log file.
|
||||
function HaskellToolsLogInternal.nvim_open_logfile()
|
||||
vim.cmd('e ' .. HaskellToolsLogInternal.get_logfile())
|
||||
end
|
||||
|
||||
local logfile, openerr
|
||||
--- @private
|
||||
--- Opens log file. Returns true if file is open, false on error
|
||||
--- @return boolean
|
||||
local function open_logfile()
|
||||
-- Try to open file only once
|
||||
if logfile then
|
||||
return true
|
||||
end
|
||||
if openerr then
|
||||
return false
|
||||
end
|
||||
|
||||
logfile, openerr = io.open(logfilename, 'a+')
|
||||
if not logfile then
|
||||
local err_msg = string.format('Failed to open haskell-tools.nvim log file: %s', openerr)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
local log_info = compat.uv.fs_stat(logfilename)
|
||||
if log_info and log_info.size > LARGE then
|
||||
local warn_msg =
|
||||
string.format('haskell-tools.nvim log is large (%d MB): %s', log_info.size / (1000 * 1000), logfilename)
|
||||
vim.notify(warn_msg, vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
-- Start message for logging
|
||||
logfile:write(string.format('[START][%s] haskell-tools.nvim logging initiated\n', os.date(log_date_format)))
|
||||
return true
|
||||
end
|
||||
|
||||
local opts = HTConfig.tools.log
|
||||
|
||||
local hls_log = HTConfig.hls.logfile
|
||||
|
||||
--- Get the haskell-language-server log file
|
||||
function HaskellToolsLogInternal.get_hls_logfile()
|
||||
return hls_log
|
||||
end
|
||||
|
||||
-- Open the haskell-language-server log file
|
||||
function HaskellToolsLogInternal.nvim_open_hls_logfile()
|
||||
vim.cmd('e ' .. HaskellToolsLogInternal.get_hls_logfile())
|
||||
end
|
||||
|
||||
local log_levels = vim.deepcopy(vim.log.levels)
|
||||
for levelstr, levelnr in pairs(log_levels) do
|
||||
log_levels[levelnr] = levelstr
|
||||
end
|
||||
|
||||
--- Set the log level
|
||||
--- @param level (string|integer) The log level
|
||||
--- @see vim.log.levels
|
||||
function HaskellToolsLogInternal.set_level(level)
|
||||
if type(level) == 'string' then
|
||||
HaskellToolsLogInternal.level =
|
||||
assert(log_levels[string.upper(level)], string.format('haskell-tools: Invalid log level: %q', level))
|
||||
else
|
||||
assert(log_levels[level], string.format('haskell-tools: Invalid log level: %d', level))
|
||||
HaskellToolsLogInternal.level = level
|
||||
end
|
||||
vim.lsp.set_log_level(HaskellToolsLogInternal.level)
|
||||
end
|
||||
|
||||
HaskellToolsLogInternal.set_level(opts.level)
|
||||
|
||||
for level, levelnr in pairs(vim.log.levels) do
|
||||
HaskellToolsLogInternal[level:lower()] = function(...)
|
||||
if HaskellToolsLogInternal.level == vim.log.levels.OFF or not open_logfile() then
|
||||
return false
|
||||
end
|
||||
local argc = select('#', ...)
|
||||
if levelnr < HaskellToolsLogInternal.level then
|
||||
return false
|
||||
end
|
||||
if argc == 0 then
|
||||
return true
|
||||
end
|
||||
local info = debug.getinfo(2, 'Sl')
|
||||
local fileinfo = string.format('%s:%s', info.short_src, info.currentline)
|
||||
local parts = {
|
||||
table.concat({ level, '|', os.date(log_date_format), '|', fileinfo, '|' }, ' '),
|
||||
}
|
||||
for i = 1, argc do
|
||||
local arg = select(i, ...)
|
||||
if arg == nil then
|
||||
table.insert(parts, '<nil>')
|
||||
elseif type(arg) == 'string' then
|
||||
table.insert(parts, arg)
|
||||
else
|
||||
table.insert(parts, format_log(arg))
|
||||
end
|
||||
end
|
||||
logfile:write(table.concat(parts, ' '), '\n')
|
||||
logfile:flush()
|
||||
end
|
||||
end
|
||||
|
||||
HaskellToolsLogInternal.debug { 'Config', HTConfig }
|
||||
|
||||
return HaskellToolsLogInternal
|
||||
@ -0,0 +1,29 @@
|
||||
---@mod haskell-tools.lsp.definition LSP textDocument/definition override
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
|
||||
local lsp_definition = {}
|
||||
|
||||
---@param opts table<string,any>|nil
|
||||
---@return nil
|
||||
function lsp_definition.mk_hoogle_fallback_definition_handler(opts)
|
||||
return function(_, result, ...)
|
||||
local ht = require('haskell-tools')
|
||||
if #result > 0 then
|
||||
local default_handler = vim.lsp.handlers['textDocument/definition']
|
||||
return default_handler(_, result, ...)
|
||||
end
|
||||
log.debug('Definition not found. Falling back to Hoogle search.')
|
||||
vim.notify('Definition not found. Falling back to Hoogle search...', vim.log.levels.WARN)
|
||||
ht.hoogle.hoogle_signature(opts or {})
|
||||
end
|
||||
end
|
||||
|
||||
return lsp_definition
|
||||
@ -0,0 +1,57 @@
|
||||
---@mod haskell-tools.lsp.eval LSP code snippet evaluation
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- General utility functions that may need to be moded somewhere else
|
||||
---@brief ]]
|
||||
local eval = {}
|
||||
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
|
||||
---@param bufnr number The buffer number
|
||||
---@return table[] The `evalCommand` lenses, in reverse order
|
||||
local function get_eval_command_lenses(bufnr, exclude_lines)
|
||||
exclude_lines = exclude_lines or {}
|
||||
local eval_cmd_lenses = {}
|
||||
for _, lens in pairs(vim.lsp.codelens.get(bufnr)) do
|
||||
if lens.command.command:match('evalCommand') and not vim.tbl_contains(exclude_lines, lens.range.start.line) then
|
||||
table.insert(eval_cmd_lenses, 1, lens)
|
||||
end
|
||||
end
|
||||
return eval_cmd_lenses
|
||||
end
|
||||
|
||||
---@param client table LSP client
|
||||
---@param lens table
|
||||
---@param bufnr number
|
||||
---@param exclude_lines number[]|nil -- (optional) `codeLens.range.start.line`s to exclude
|
||||
---@return nil
|
||||
local function go(client, bufnr, lens, exclude_lines)
|
||||
local command = lens.command
|
||||
client.request_sync('workspace/executeCommand', command, 1000, bufnr)
|
||||
exclude_lines[#exclude_lines + 1] = lens.range.start.line
|
||||
local new_lenses = get_eval_command_lenses(bufnr, exclude_lines)
|
||||
if #new_lenses > 0 then
|
||||
go(client, bufnr, new_lenses[1], exclude_lines)
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr number|nil The buffer number
|
||||
---@return nil
|
||||
function eval.all(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_win_get_buf(0)
|
||||
local clients = LspHelpers.get_active_ht_clients(bufnr)
|
||||
if not clients or #clients == 0 then
|
||||
return
|
||||
end
|
||||
local client = clients[1]
|
||||
local lenses = get_eval_command_lenses(bufnr)
|
||||
if #lenses > 0 then
|
||||
go(client, bufnr, lenses[1], {})
|
||||
vim.lsp.codelens.refresh()
|
||||
end
|
||||
end
|
||||
|
||||
return eval
|
||||
@ -0,0 +1,98 @@
|
||||
---@mod haskell-tools.lsp.helpers
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- LSP helper functions
|
||||
---@brief ]]
|
||||
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
|
||||
---@class LspHelpers
|
||||
local LspHelpers = {}
|
||||
|
||||
local compat = require('haskell-tools.compat')
|
||||
LspHelpers.get_clients = compat.get_clients
|
||||
|
||||
LspHelpers.haskell_client_name = 'haskell-tools.nvim'
|
||||
LspHelpers.cabal_client_name = 'haskell-tools.nvim (cabal)'
|
||||
|
||||
---@param bufnr number the buffer to get clients for
|
||||
---@return lsp.Client[] haskell_clients
|
||||
---@see util.get_clients
|
||||
function LspHelpers.get_active_haskell_clients(bufnr)
|
||||
return LspHelpers.get_clients { bufnr = bufnr, name = LspHelpers.haskell_client_name }
|
||||
end
|
||||
|
||||
---@param bufnr number the buffer to get clients for
|
||||
---@return lsp.Client[] cabal_clinets
|
||||
---@see util.get_clients
|
||||
function LspHelpers.get_active_cabal_clients(bufnr)
|
||||
return LspHelpers.get_clients { bufnr = bufnr, name = LspHelpers.cabal_client_name }
|
||||
end
|
||||
|
||||
---@param bufnr number the buffer to get clients for
|
||||
---@return lsp.Client[] ht_clients The haskell + cabal clients
|
||||
---@see util.get_clients
|
||||
---@see util.get_active_haskell_clients
|
||||
---@see util.get_active_cabal_clients
|
||||
function LspHelpers.get_active_ht_clients(bufnr)
|
||||
local clients = {}
|
||||
vim.list_extend(clients, LspHelpers.get_active_haskell_clients(bufnr))
|
||||
vim.list_extend(clients, LspHelpers.get_active_cabal_clients(bufnr))
|
||||
return clients
|
||||
end
|
||||
|
||||
---@return string[] cmd The command to invoke haskell-language-server
|
||||
LspHelpers.get_hls_cmd = function()
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local cmd = Types.evaluate(HTConfig.hls.cmd)
|
||||
---@cast cmd string[]
|
||||
assert(type(cmd) == 'table', 'haskell-tools: hls.cmd should evaluate to a string[]')
|
||||
assert(#cmd > 1, 'haskell-tools: hls.cmd evaluates to an empty list.')
|
||||
return cmd
|
||||
end
|
||||
|
||||
---Returns nil if the hls version cannot be determined.
|
||||
---@return number[]|nil hls_version The haskell-language-server version
|
||||
local function get_hls_version()
|
||||
local hls_bin = LspHelpers.get_hls_cmd()[1]
|
||||
if vim.fn.executable(hls_bin) ~= 1 then
|
||||
return nil
|
||||
end
|
||||
local handle = io.popen(hls_bin .. ' --version')
|
||||
if not handle then
|
||||
return nil
|
||||
end
|
||||
local output, error_msg = handle:read('*a')
|
||||
handle:close()
|
||||
if error_msg then
|
||||
return nil
|
||||
end
|
||||
local version_str = output:match('version:%s([^%s]*)%s.*')
|
||||
if not version_str then
|
||||
return nil
|
||||
end
|
||||
local function parse_version()
|
||||
local version = {}
|
||||
for str in string.gmatch(version_str, '([^%.]+)') do
|
||||
table.insert(version, tonumber(str))
|
||||
end
|
||||
return #version > 1 and version
|
||||
end
|
||||
local ok, version = pcall(parse_version)
|
||||
return ok and version or nil
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
LspHelpers.is_hls_version_with_cabal_support = function()
|
||||
local version = get_hls_version()
|
||||
-- XXX: If the version cannot be parsed, we assume it supports
|
||||
--- cabal (in case there is a newer version that we cannot
|
||||
--- parse the version from).
|
||||
return version == nil or version[1] > 1 or version[2] >= 9
|
||||
end
|
||||
|
||||
return LspHelpers
|
||||
@ -0,0 +1,312 @@
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Inspired by rust-tools.nvim's hover_actions
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local lsp_util = vim.lsp.util
|
||||
local HtParser = require('haskell-tools.parser')
|
||||
local OS = require('haskell-tools.os')
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
|
||||
local hover = {}
|
||||
|
||||
---@class HtHoverState
|
||||
---@field winnr number|nil The hover window number
|
||||
---@field commands (fun():nil)[] List of hover actions
|
||||
|
||||
---@type HtHoverState
|
||||
local _state = {
|
||||
winnr = nil,
|
||||
commands = {},
|
||||
}
|
||||
|
||||
---@return nil
|
||||
local function close_hover()
|
||||
local winnr = _state.winnr
|
||||
if winnr ~= nil and vim.api.nvim_win_is_valid(winnr) then
|
||||
vim.api.nvim_win_close(winnr, true)
|
||||
_state.winnr = nil
|
||||
_state.commands = {}
|
||||
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]
|
||||
|
||||
if line > #_state.commands then
|
||||
return
|
||||
end
|
||||
local action = _state.commands[line]
|
||||
close_hover()
|
||||
action()
|
||||
end
|
||||
|
||||
---@param x string hex
|
||||
---@return string char
|
||||
local function hex_to_char(x)
|
||||
return string.char(tonumber(x, 16))
|
||||
end
|
||||
|
||||
---Formats a location in a Haskell file, shortening it to a relative path if possible.
|
||||
---@param location string The location provided by LSP hover
|
||||
---@param current_file string The current file path or an empty string
|
||||
---@return string formatted_location or the original location if the file is not a Haskell file
|
||||
local function format_location(location, current_file)
|
||||
-- remove *
|
||||
-- replace quotes with markdown backticks
|
||||
-- decode url-encoded characters
|
||||
local formatted_location = ('%s')
|
||||
:format(location)
|
||||
:gsub('%*', '') -- remove *
|
||||
:gsub('‘', '`') -- markdown backticks
|
||||
:gsub('’', '`')
|
||||
:gsub('%%(%x%x)', hex_to_char) -- decode url-encoded characters
|
||||
local file_location = formatted_location:match('(.*).hs:')
|
||||
if not file_location then
|
||||
return formatted_location
|
||||
end
|
||||
local is_current_buf = formatted_location:find(current_file, 1, true) == 1
|
||||
if is_current_buf then
|
||||
return formatted_location:sub(#current_file + 2)
|
||||
end
|
||||
local path = file_location .. '.hs'
|
||||
local package_path = HtProjectHelpers.match_package_root(path)
|
||||
if package_path then
|
||||
return formatted_location:sub(#package_path + 2) -- trim package path + first '/'
|
||||
end
|
||||
local project_path = HtProjectHelpers.match_project_root(path)
|
||||
if project_path then
|
||||
formatted_location = formatted_location:sub(#project_path + 2):gsub('/', ':', 1) -- trim project path + first '/'
|
||||
end
|
||||
return formatted_location
|
||||
end
|
||||
|
||||
---@param result table LSP result
|
||||
---@return string location string
|
||||
local function mk_location(result)
|
||||
local range_start = result.range and result.range.start or {}
|
||||
local line = range_start.line
|
||||
local character = range_start.character
|
||||
local uri = result.uri and result.uri:gsub('file://', '')
|
||||
return line and character and uri and uri .. ':' .. tostring(line + 1) .. ':' .. tostring(character + 1) or ''
|
||||
end
|
||||
|
||||
---Is the result's start location the same as the params location?
|
||||
---@param params table LSP location params
|
||||
---@param result table LSP result
|
||||
---@return boolean
|
||||
local function is_same_position(params, result)
|
||||
local range_start = result.range and result.range.start or {}
|
||||
return params.textDocument.uri == result.uri
|
||||
and params.position.line == range_start.line
|
||||
and params.position.character == range_start.character
|
||||
end
|
||||
|
||||
---LSP handler for textDocument/hover
|
||||
---@param result table
|
||||
---@param ctx table
|
||||
---@param config table<string,any>|nil
|
||||
---@return number|nil bufnr
|
||||
---@return number|nil winnr
|
||||
function hover.on_hover(_, result, ctx, config)
|
||||
local ht = require('haskell-tools')
|
||||
config = config or {}
|
||||
config.focus_id = ctx.method
|
||||
if vim.api.nvim_get_current_buf() ~= ctx.bufnr then
|
||||
-- Ignore result since buffer changed.
|
||||
return
|
||||
end
|
||||
if not (result and result.contents) then
|
||||
vim.notify('No information available')
|
||||
return
|
||||
end
|
||||
local markdown_lines = lsp_util.convert_input_to_markdown_lines(result.contents)
|
||||
if vim.tbl_isempty(markdown_lines) then
|
||||
log.debug('No hover information available.')
|
||||
vim.notify('No information available')
|
||||
return
|
||||
end
|
||||
local to_remove = {}
|
||||
local actions = {}
|
||||
_state.commands = {}
|
||||
local func_name = vim.fn.expand('<cword>')
|
||||
---@cast func_name string
|
||||
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()
|
||||
log.debug { 'Hover: Hoogle search for signature', signature }
|
||||
ht.hoogle.hoogle_signature { search_term = signature }
|
||||
end)
|
||||
end
|
||||
local cword = vim.fn.expand('<cword>')
|
||||
table.insert(actions, 1, string.format('%d. Hoogle search: `%s`', #actions + 1, cword))
|
||||
table.insert(_state.commands, function()
|
||||
log.debug { 'Hover: Hoogle search for cword', cword }
|
||||
ht.hoogle.hoogle_signature { search_term = cword }
|
||||
end)
|
||||
local params = ctx.params
|
||||
local found_location = false
|
||||
local found_type_definition = false
|
||||
local found_documentation = false
|
||||
local found_source = false
|
||||
for i, value in ipairs(markdown_lines) do
|
||||
if vim.startswith(value, '[Documentation]') and not found_documentation then
|
||||
found_documentation = true
|
||||
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()
|
||||
log.debug { 'Hover: Open documentation in browser', uri }
|
||||
OS.open_browser(uri)
|
||||
end)
|
||||
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()
|
||||
log.debug { 'Hover: View source in browser', uri }
|
||||
OS.open_browser(uri)
|
||||
end)
|
||||
end
|
||||
local location = string.match(value, '*Defined [ia][nt] (.+)')
|
||||
local current_file = params.textDocument.uri:gsub('file://', '')
|
||||
local results, err, definition_results
|
||||
if location == nil or found_location then
|
||||
goto SkipDefinition
|
||||
end
|
||||
found_location = true
|
||||
table.insert(to_remove, 1, i)
|
||||
results, err = vim.lsp.buf_request_sync(0, 'textDocument/definition', params, 1000)
|
||||
if err or results == nil or #results == 0 then
|
||||
goto SkipDefinition
|
||||
end
|
||||
definition_results = results[1] and results[1].result or {}
|
||||
if #definition_results > 0 then
|
||||
local location_suffix = ('%s'):format(format_location(location, current_file))
|
||||
local definition_result = definition_results[1]
|
||||
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()
|
||||
-- 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',
|
||||
})
|
||||
log.debug { 'Hover: Go to definition', definition_result }
|
||||
---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)
|
||||
end
|
||||
else -- Display Hoogle search instead
|
||||
local pkg = location:match('‘(.+)’')
|
||||
local search_term = pkg and pkg .. '.' .. cword or cword
|
||||
table.insert(actions, 1, string.format('%d. Hoogle search: `%s`', #actions + 1, search_term))
|
||||
table.insert(_state.commands, function()
|
||||
log.debug { 'Hover: Hoogle search for definition', search_term }
|
||||
ht.hoogle.hoogle_signature { search_term = search_term }
|
||||
end)
|
||||
end
|
||||
table.insert(actions, 1, string.format('%d. Find references', #actions + 1))
|
||||
table.insert(_state.commands, 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)
|
||||
::SkipDefinition::
|
||||
if found_type_definition then
|
||||
goto SkipTypeDefinition
|
||||
end
|
||||
results, err = vim.lsp.buf_request_sync(0, 'textDocument/typeDefinition', params, 1000)
|
||||
if err or results == nil or #results == 0 then -- Can go to type definition
|
||||
goto SkipTypeDefinition
|
||||
end
|
||||
found_type_definition = true
|
||||
local type_definition_results = results[1] and results[1].result or {}
|
||||
if #type_definition_results == 0 then
|
||||
goto SkipTypeDefinition
|
||||
end
|
||||
local type_definition_result = type_definition_results[1]
|
||||
local type_def_suffix = format_location(mk_location(type_definition_result), current_file)
|
||||
if is_same_position(params, result) then
|
||||
goto SkipTypeDefinition
|
||||
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()
|
||||
-- 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',
|
||||
})
|
||||
log.debug { 'Hover: Go to type definition', type_definition_result }
|
||||
---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)
|
||||
::SkipTypeDefinition::
|
||||
end
|
||||
for _, pos in ipairs(to_remove) do
|
||||
table.remove(markdown_lines, pos)
|
||||
end
|
||||
for _, action in ipairs(actions) do
|
||||
table.insert(markdown_lines, 1, action)
|
||||
end
|
||||
if #actions > 0 then
|
||||
table.insert(markdown_lines, #actions + 1, '')
|
||||
table.insert(markdown_lines, #actions + 1, '')
|
||||
end
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local opts = HTConfig.tools.hover
|
||||
config = vim.tbl_extend('keep', {
|
||||
border = opts.border,
|
||||
stylize_markdown = opts.stylize_markdown,
|
||||
focusable = true,
|
||||
focus_id = 'haskell-tools-hover',
|
||||
close_events = { 'CursorMoved', 'BufHidden', 'InsertCharPre' },
|
||||
}, config or {})
|
||||
local bufnr, winnr = lsp_util.open_floating_preview(markdown_lines, 'markdown', config)
|
||||
if opts.stylize_markdown == false then
|
||||
vim.bo[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
|
||||
end
|
||||
|
||||
_state.winnr = winnr
|
||||
vim.keymap.set('n', '<Esc>', close_hover, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.api.nvim_buf_attach(bufnr, false, {
|
||||
on_detach = function()
|
||||
_state.winnr = nil
|
||||
end,
|
||||
})
|
||||
|
||||
if #_state.commands == 0 then
|
||||
return 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 })
|
||||
|
||||
return bufnr, winnr
|
||||
end
|
||||
|
||||
return hover
|
||||
@ -0,0 +1,265 @@
|
||||
---@mod haskell-tools.lsp haskell-language-server LSP client tools
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@brief [[
|
||||
--- The following commands are available:
|
||||
---
|
||||
--- * `:HlsStart` - Start the LSP client.
|
||||
--- * `:HlsStop` - Stop the LSP client.
|
||||
--- * `:HlsRestart` - Restart the LSP client.
|
||||
--- * `:HlsEvalAll` - Evaluate all code snippets in comments.
|
||||
---@brief ]]
|
||||
|
||||
---To minimise the risk of this occurring, we attempt to shut down hls cleanly before exiting neovim.
|
||||
---@param client lsp.Client The LSP client
|
||||
---@param bufnr number The buffer number
|
||||
---@return nil
|
||||
local function ensure_clean_exit_on_quit(client, bufnr)
|
||||
vim.api.nvim_create_autocmd('VimLeavePre', {
|
||||
group = vim.api.nvim_create_augroup('haskell-tools-hls-clean-exit-' .. tostring(client.id), { clear = true }),
|
||||
callback = function()
|
||||
log.debug('Stopping LSP client...')
|
||||
vim.lsp.stop_client(client, false)
|
||||
end,
|
||||
buffer = bufnr,
|
||||
})
|
||||
end
|
||||
|
||||
---A workaround for #48:
|
||||
---Some plugins that add LSP client capabilities which are not built-in to neovim
|
||||
---(like nvim-ufo and nvim-lsp-selection-range) cause error messages, because
|
||||
---haskell-language-server falsly advertises those server_capabilities for cabal files.
|
||||
---@param client lsp.Client
|
||||
---@return nil
|
||||
local function fix_cabal_client(client)
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
if client.name == LspHelpers.cabal_client_name and client.server_capabilities then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
client.server_capabilities = vim.tbl_extend('force', client.server_capabilities, {
|
||||
foldingRangeProvider = false,
|
||||
selectionRangeProvider = false,
|
||||
documentHighlightProvider = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@class LoadHlsSettingsOpts
|
||||
---@field settings_file_pattern string|nil File name or pattern to search for. Defaults to 'hls.json'
|
||||
|
||||
log.debug('Setting up the LSP client...')
|
||||
local hls_opts = HTConfig.hls
|
||||
local handlers = {}
|
||||
|
||||
local tools_opts = HTConfig.tools
|
||||
local definition_opts = tools_opts.definition or {}
|
||||
|
||||
if Types.evaluate(definition_opts.hoogle_signature_fallback) then
|
||||
local lsp_definition = require('haskell-tools.lsp.definition')
|
||||
log.debug('Wrapping vim.lsp.buf.definition with Hoogle signature fallback.')
|
||||
handlers['textDocument/definition'] = lsp_definition.mk_hoogle_fallback_definition_handler(definition_opts)
|
||||
end
|
||||
local hover_opts = tools_opts.hover
|
||||
if Types.evaluate(hover_opts.enable) then
|
||||
local hover = require('haskell-tools.lsp.hover')
|
||||
handlers['textDocument/hover'] = hover.on_hover
|
||||
end
|
||||
|
||||
---@class HlsTools
|
||||
local HlsTools = {}
|
||||
---Search the project root for a haskell-language-server settings JSON file and load it to a Lua table.
|
||||
---Falls back to the `hls.default_settings` if no file is found or file cannot be read or decoded.
|
||||
---@param project_root string|nil The project root
|
||||
---@param opts LoadHlsSettingsOpts|nil
|
||||
---@return table hls_settings
|
||||
---@see https://haskell-language-server.readthedocs.io/en/latest/configuration.html
|
||||
HlsTools.load_hls_settings = function(project_root, opts)
|
||||
local default_settings = HTConfig.hls.default_settings
|
||||
if not project_root then
|
||||
return default_settings
|
||||
end
|
||||
local default_opts = { settings_file_pattern = 'hls.json' }
|
||||
opts = vim.tbl_deep_extend('force', {}, default_opts, opts or {})
|
||||
local results = vim.fn.glob(compat.joinpath(project_root, opts.settings_file_pattern), true, true)
|
||||
if #results == 0 then
|
||||
log.info(opts.settings_file_pattern .. ' not found in project root ' .. project_root)
|
||||
return default_settings
|
||||
end
|
||||
local settings_json = results[1]
|
||||
local OS = require('haskell-tools.os')
|
||||
local content = OS.read_file(settings_json)
|
||||
local success, settings = pcall(vim.json.decode, content)
|
||||
if not success then
|
||||
local msg = 'Could not decode ' .. settings_json .. '. Falling back to default settings.'
|
||||
log.warn { msg, error }
|
||||
vim.notify('haskell-tools.lsp: ' .. msg, vim.log.levels.WARN)
|
||||
return default_settings
|
||||
end
|
||||
log.debug { 'hls settings', settings }
|
||||
return settings or default_settings
|
||||
end
|
||||
|
||||
---Start or attach the LSP client.
|
||||
---Fails silently if the buffer's filetype is not one of the filetypes specified in the config.
|
||||
---@param bufnr number|nil The buffer number (optional), defaults to the current buffer
|
||||
---@return number|nil client_id The LSP client ID
|
||||
HlsTools.start = function(bufnr)
|
||||
local ht = require('haskell-tools')
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local file = vim.api.nvim_buf_get_name(bufnr)
|
||||
if not file or #file == 0 then
|
||||
local msg = 'Could not determine the name of buffer ' .. bufnr .. '.'
|
||||
log.debug('lsp.start: ' .. msg)
|
||||
return
|
||||
end
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local is_cabal = HtProjectHelpers.is_cabal_file(bufnr)
|
||||
local project_root = ht.project.root_dir(file)
|
||||
local hls_settings = type(hls_opts.settings) == 'function' and hls_opts.settings(project_root) or hls_opts.settings
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
local cmd = LspHelpers.get_hls_cmd()
|
||||
local hls_bin = cmd[1]
|
||||
if vim.fn.executable(hls_bin) == 0 then
|
||||
log.warn('Executable ' .. hls_bin .. ' not found.')
|
||||
end
|
||||
|
||||
local lsp_start_opts = {
|
||||
name = is_cabal and LspHelpers.cabal_client_name or LspHelpers.haskell_client_name,
|
||||
cmd = Types.evaluate(cmd),
|
||||
root_dir = project_root,
|
||||
filetypes = is_cabal and { 'cabal', 'cabalproject' } or { 'haskell', 'lhaskell' },
|
||||
capabilities = hls_opts.capabilities,
|
||||
handlers = handlers,
|
||||
settings = hls_settings,
|
||||
on_attach = function(client_id, buf)
|
||||
log.debug('LSP attach')
|
||||
local ok, err = pcall(hls_opts.on_attach, client_id, buf, ht)
|
||||
if not ok then
|
||||
---@cast err string
|
||||
log.error { 'on_attach failed', err }
|
||||
vim.notify('haskell-tools.lsp: Error in hls.on_attach: ' .. err)
|
||||
end
|
||||
local function buf_refresh_codeLens()
|
||||
vim.schedule(function()
|
||||
for _, client in pairs(LspHelpers.get_active_ht_clients(bufnr)) do
|
||||
if client.server_capabilities.codeLensProvider then
|
||||
vim.lsp.codelens.refresh()
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
local code_lens_opts = tools_opts.codeLens or {}
|
||||
if Types.evaluate(code_lens_opts.autoRefresh) then
|
||||
vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufWritePost', 'TextChanged' }, {
|
||||
group = vim.api.nvim_create_augroup('haskell-tools-code-lens', {}),
|
||||
callback = buf_refresh_codeLens,
|
||||
buffer = buf,
|
||||
})
|
||||
buf_refresh_codeLens()
|
||||
end
|
||||
end,
|
||||
on_init = function(client, _)
|
||||
ensure_clean_exit_on_quit(client, bufnr)
|
||||
fix_cabal_client(client)
|
||||
end,
|
||||
}
|
||||
log.debug('LSP start options: lsp_start_opts')
|
||||
local client_id = vim.lsp.start(lsp_start_opts)
|
||||
return client_id
|
||||
end
|
||||
|
||||
---Stop the LSP client.
|
||||
---@param bufnr number|nil The buffer number (optional), defaults to the current buffer
|
||||
---@return table[] clients A list of clients that will be stopped
|
||||
HlsTools.stop = function(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local LspHelpers = require('haskell-tools.lsp.helpers')
|
||||
local clients = LspHelpers.get_active_ht_clients(bufnr)
|
||||
vim.lsp.stop_client(clients)
|
||||
return clients
|
||||
end
|
||||
|
||||
---Restart the LSP client.
|
||||
---Fails silently if the buffer's filetype is not one of the filetypes specified in the config.
|
||||
---@param bufnr number|nil The buffer number (optional), defaults to the current buffer
|
||||
---@return number|nil client_id The LSP client ID after restart
|
||||
HlsTools.restart = function(bufnr)
|
||||
local lsp = require('haskell-tools').lsp
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local clients = lsp.stop(bufnr)
|
||||
local timer, err_name, err_msg = compat.uv.new_timer()
|
||||
if not timer then
|
||||
log.error { 'Could not create timer', err_name, err_msg }
|
||||
return
|
||||
end
|
||||
local attempts_to_live = 50
|
||||
local stopped_client_count = 0
|
||||
timer:start(200, 100, function()
|
||||
for _, client in ipairs(clients) do
|
||||
if client:is_stopped() then
|
||||
stopped_client_count = stopped_client_count + 1
|
||||
vim.schedule(function()
|
||||
lsp.start(bufnr)
|
||||
end)
|
||||
end
|
||||
end
|
||||
if stopped_client_count >= #clients then
|
||||
timer:stop()
|
||||
attempts_to_live = 0
|
||||
elseif attempts_to_live <= 0 then
|
||||
vim.notify('haslell-tools.lsp: Could not restart all LSP clients.', vim.log.levels.ERROR)
|
||||
timer:stop()
|
||||
attempts_to_live = 0
|
||||
end
|
||||
attempts_to_live = attempts_to_live - 1
|
||||
end)
|
||||
end
|
||||
|
||||
---Evaluate all code snippets in comments.
|
||||
---@param bufnr number|nil Defaults to the current buffer.
|
||||
---@return nil
|
||||
HlsTools.buf_eval_all = function(bufnr)
|
||||
local eval = require('haskell-tools.lsp.eval')
|
||||
return eval.all(bufnr)
|
||||
end
|
||||
|
||||
local commands = {
|
||||
{
|
||||
'HlsStart',
|
||||
function()
|
||||
HlsTools.start()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HlsStop',
|
||||
function()
|
||||
HlsTools.stop()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HlsRestart',
|
||||
function()
|
||||
HlsTools.restart()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HlsEvalAll',
|
||||
function()
|
||||
HlsTools.buf_eval_all()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, command in ipairs(commands) do
|
||||
vim.api.nvim_create_user_command(unpack(command))
|
||||
end
|
||||
|
||||
return HlsTools
|
||||
@ -0,0 +1,80 @@
|
||||
---@mod haskell-tools.os
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Functions for interacting with the operating system
|
||||
---@brief ]]
|
||||
|
||||
local compat = require('haskell-tools.compat')
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local uv = compat.uv
|
||||
|
||||
---@class OS
|
||||
local OS = {}
|
||||
|
||||
---@param url string
|
||||
---@return nil
|
||||
OS.open_browser = function(url)
|
||||
local browser_cmd
|
||||
if vim.fn.has('unix') == 1 then
|
||||
if vim.fn.executable('sensible-browser') == 1 then
|
||||
browser_cmd = 'sensible-browser'
|
||||
else
|
||||
browser_cmd = 'xdg-open'
|
||||
end
|
||||
end
|
||||
if vim.fn.has('mac') == 1 then
|
||||
browser_cmd = 'open'
|
||||
end
|
||||
if browser_cmd and vim.fn.executable(browser_cmd) == 1 then
|
||||
local cmd = { browser_cmd, url }
|
||||
log.debug { 'Opening browser', cmd }
|
||||
compat.system(cmd, nil, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
if sc.code ~= 0 then
|
||||
log.error { 'Error opening browser', sc.code, sc.stderr }
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
local msg = 'No executable found to open the browser.'
|
||||
log.error(msg)
|
||||
vim.notify('haskell-tools.hoogle: ' .. msg, vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
---Read the content of a file
|
||||
---@param filename string
|
||||
---@return string|nil content
|
||||
OS.read_file = function(filename)
|
||||
local content
|
||||
local f = io.open(filename, 'r')
|
||||
if f then
|
||||
content = f:read('*a')
|
||||
f:close()
|
||||
end
|
||||
return content
|
||||
end
|
||||
|
||||
---Asynchronously the content of a file
|
||||
---@param filename string
|
||||
---@return string|nil content
|
||||
---@async
|
||||
OS.read_file_async = function(filename)
|
||||
local file_fd = uv.fs_open(filename, 'r', 438)
|
||||
if not file_fd then
|
||||
return nil
|
||||
end
|
||||
local stat = uv.fs_fstat(file_fd)
|
||||
if not stat then
|
||||
return nil
|
||||
end
|
||||
local data = uv.fs_read(file_fd, stat.size, 0)
|
||||
uv.fs_close(file_fd)
|
||||
---@cast data string?
|
||||
return data
|
||||
end
|
||||
|
||||
return OS
|
||||
@ -0,0 +1,52 @@
|
||||
---@mod haskell-tools.parser
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Parsing functions
|
||||
---@brief ]]
|
||||
|
||||
---@class HtParser
|
||||
local HtParser = {}
|
||||
|
||||
--- Pretty-print a type signature
|
||||
--- @param sig string|nil The raw signature
|
||||
--- @return string|nil pp_sig The pretty-printed signature
|
||||
local function pp_signature(sig)
|
||||
local pp_sig = sig
|
||||
and sig
|
||||
:gsub('\n', ' ') -- join lines
|
||||
:gsub('forall .*%.%s', '') -- hoogle cannot search for `forall a.`
|
||||
:gsub('^%s*(.-)%s*$', '%1') -- trim
|
||||
return pp_sig
|
||||
end
|
||||
|
||||
--- Get the type signature of the word under the cursor from markdown
|
||||
--- @param func_name string the name of the function
|
||||
--- @param docs string Markdown docs
|
||||
--- @return string|nil function_signature Type signature, or the word under the cursor if none was found
|
||||
--- @return string[] signatures Other type signatures returned by hls
|
||||
HtParser.try_get_signatures_from_markdown = function(func_name, docs)
|
||||
local all_sigs = {}
|
||||
---@type string|nil
|
||||
local raw_func_sig = docs:match('```haskell\n' .. func_name .. '%s::%s([^```]*)')
|
||||
for code_block in docs:gmatch('```haskell\n([^```]*)\n```') do
|
||||
---@type string|nil
|
||||
local sig = code_block:match('::%s([^```]*)')
|
||||
local pp_sig = sig and pp_signature(sig)
|
||||
if sig and not vim.tbl_contains(all_sigs, pp_sig) then
|
||||
table.insert(all_sigs, pp_sig)
|
||||
end
|
||||
end
|
||||
return raw_func_sig and pp_signature(raw_func_sig), all_sigs
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return integer indent
|
||||
HtParser.get_indent = function(str)
|
||||
return #(str:match('^(%s+)%S') or '')
|
||||
end
|
||||
|
||||
return HtParser
|
||||
@ -0,0 +1,124 @@
|
||||
---@mod haskell-tools.cabal
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Helper functions related to cabal projects
|
||||
---@brief ]]
|
||||
|
||||
local Strings = require('haskell-tools.strings')
|
||||
local HtParser = require('haskell-tools.parser')
|
||||
local Dap = require('haskell-tools.dap.internal')
|
||||
local OS = require('haskell-tools.os')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class CabalProjectHelper
|
||||
local CabalProjectHelper = {}
|
||||
|
||||
---@class CabalEntryPointParserData
|
||||
---@field idx integer
|
||||
---@field lines string[]
|
||||
---@field line string
|
||||
---@field package_dir string
|
||||
|
||||
---@class CabalEntryPointParserState
|
||||
---@field package_name string
|
||||
---@field entry_points HsEntryPoint[]
|
||||
---@field mains string[]
|
||||
---@field source_dirs string[]
|
||||
---@field src_dir_indent_pattern string
|
||||
---@field exe_name string | nil
|
||||
|
||||
---@param data CabalEntryPointParserData
|
||||
---@param state CabalEntryPointParserState
|
||||
local function get_entrypoint_from_line(data, state)
|
||||
local package_dir = data.package_dir
|
||||
local idx = data.idx
|
||||
local lines = data.lines
|
||||
local line = data.line
|
||||
state.package_name = state.package_name or line:match('^name:%s*(.+)')
|
||||
local no_indent = HtParser.get_indent(line) == 0
|
||||
if no_indent or idx == #lines then
|
||||
vim.list_extend(
|
||||
state.entry_points,
|
||||
Dap.mk_entry_points(state.package_name, state.exe_name, package_dir, state.mains, state.source_dirs)
|
||||
)
|
||||
state.mains = {}
|
||||
state.source_dirs = {}
|
||||
state.exe_name = nil
|
||||
end
|
||||
state.exe_name = state.exe_name or line:match('^%S+%s+(.+)') or state.package_name
|
||||
-- main detection
|
||||
local main = line:match('main%-is:%s+(.+)%.hs')
|
||||
if not main and lines[idx + 1] and line:match('main%-is:') then
|
||||
main = (lines[idx + 1]):match('%s+(.+)%.hs')
|
||||
end
|
||||
if main then
|
||||
table.insert(state.mains, main .. '.hs')
|
||||
end
|
||||
-- source directory detection
|
||||
local is_src_dir_end = state.src_dir_indent_pattern and (line == '' or line:match(state.src_dir_indent_pattern))
|
||||
if is_src_dir_end then
|
||||
state.src_dir_indent_pattern = nil
|
||||
end
|
||||
if state.src_dir_indent_pattern then
|
||||
local source_dir = line:match(',%s*(.*)') or line:match('%s+(.*)')
|
||||
if source_dir then
|
||||
table.insert(state.source_dirs, source_dir)
|
||||
end
|
||||
else
|
||||
local source_dirs_indent = line:match('(%s*)hs%-source%-dirs:')
|
||||
if source_dirs_indent then
|
||||
state.src_dir_indent_pattern = '^' .. ('%s'):rep(#source_dirs_indent) .. '%S+'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Parse the DAP entry points from a *.cabal file
|
||||
---@param package_file string Path to the *.cabal file
|
||||
---@return HsEntryPoint[] entry_points
|
||||
---@async
|
||||
local function parse_package_entrypoints(package_file)
|
||||
local state = {
|
||||
entry_points = {},
|
||||
mains = {},
|
||||
source_dirs = {},
|
||||
}
|
||||
local package_dir = vim.fn.fnamemodify(package_file, ':h') or package_file
|
||||
local entry_points = {}
|
||||
local content = OS.read_file_async(package_file)
|
||||
if not content then
|
||||
return entry_points
|
||||
end
|
||||
local lines = vim.split(content, '\n') or {}
|
||||
for idx, line in ipairs(lines) do
|
||||
local is_comment = vim.startswith(Strings.trim(line), '--')
|
||||
if not is_comment then
|
||||
---@type CabalEntryPointParserData
|
||||
local data = {
|
||||
package_dir = package_dir,
|
||||
line = line,
|
||||
lines = lines,
|
||||
idx = idx,
|
||||
}
|
||||
get_entrypoint_from_line(data, state)
|
||||
end
|
||||
end
|
||||
return state.entry_points
|
||||
end
|
||||
|
||||
---Parse the DAP entry points from a *.cabal file
|
||||
---@param package_path string Path to a package directory
|
||||
---@return HsEntryPoint[] entry_points
|
||||
---@async
|
||||
function CabalProjectHelper.parse_package_entrypoints(package_path)
|
||||
local entry_points = {}
|
||||
for _, package_file in pairs(vim.fn.glob(compat.joinpath(package_path, '*.cabal'), true, true)) do
|
||||
vim.list_extend(entry_points, parse_package_entrypoints(package_file))
|
||||
end
|
||||
return entry_points
|
||||
end
|
||||
|
||||
return CabalProjectHelper
|
||||
@ -0,0 +1,264 @@
|
||||
---@mod haskell-tools.project
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Utility functions for analysing a project.
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local Strings = require('haskell-tools.strings')
|
||||
local OS = require('haskell-tools.os')
|
||||
local cabal = require('haskell-tools.project.cabal')
|
||||
local stack = require('haskell-tools.project.stack')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class HtProjectHelpers
|
||||
local HtProjectHelpers = {}
|
||||
|
||||
---@param path string
|
||||
---@return string stripped_path For zipfile: or tarfile: virtual paths, returns the path to the archive. Other paths are returned unaltered.
|
||||
--- Taken from nvim-lspconfig
|
||||
local function strip_archive_subpath(path)
|
||||
-- Matches regex from zip.vim / tar.vim
|
||||
path = vim.fn.substitute(path, 'zipfile://\\(.\\{-}\\)::[^\\\\].*$', '\\1', '') or path
|
||||
path = vim.fn.substitute(path, 'tarfile:\\(.\\{-}\\)::.*$', '\\1', '') or path
|
||||
return path
|
||||
end
|
||||
|
||||
---@param path string the file path to search in
|
||||
---@param ... string Search patterns (can be globs)
|
||||
---@return string|nil The first file that matches the globs
|
||||
local function find_file(path, ...)
|
||||
for _, search_term in ipairs(compat.tbl_flatten { ... }) do
|
||||
local results = vim.fn.glob(compat.joinpath(path, search_term), true, true)
|
||||
if #results > 0 then
|
||||
return results[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Iterate the path until we find the rootdir.
|
||||
---@param startpath string The start path
|
||||
---@return fun(_:any,path:string):(string?,string?)
|
||||
---@return string startpath
|
||||
---@return string startpath
|
||||
local function iterate_parents(startpath)
|
||||
---@param _ any Ignored
|
||||
---@param path string file path
|
||||
---@return string|nil path
|
||||
---@return string|nil startpath
|
||||
local function it(_, path)
|
||||
local next = vim.fn.fnamemodify(path, ':h')
|
||||
if not next or vim.fn.isdirectory(next) == 0 or next == path or next == '/nix/store' then
|
||||
return
|
||||
end
|
||||
if compat.uv.fs_realpath(next) then
|
||||
return next, startpath
|
||||
end
|
||||
end
|
||||
return it, startpath, startpath
|
||||
end
|
||||
|
||||
---@param startpath string The start path to search upward from
|
||||
---@param matcher fun(path:string):string|nil
|
||||
---@return string|nil
|
||||
local function search_ancestors(startpath, matcher)
|
||||
if matcher(startpath) then
|
||||
return startpath
|
||||
end
|
||||
local max_iterations = 100
|
||||
for path in iterate_parents(startpath) do
|
||||
max_iterations = max_iterations - 1
|
||||
if max_iterations == 0 then
|
||||
return
|
||||
end
|
||||
if not path then
|
||||
return
|
||||
end
|
||||
if matcher(path) then
|
||||
return path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param ... string Globs to match in the root directory
|
||||
---@return fun(path:string):(string|nil)
|
||||
local function root_pattern(...)
|
||||
local args = compat.tbl_flatten { ... }
|
||||
local function matcher(path)
|
||||
return find_file(path, unpack(args))
|
||||
end
|
||||
return function(path)
|
||||
local startpath = strip_archive_subpath(path)
|
||||
return search_ancestors(startpath, matcher)
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string escaped_path
|
||||
local function escape_glob_wildcards(path)
|
||||
local escaped_path = path:gsub('([%[%]%?%*])', '\\%1')
|
||||
return escaped_path
|
||||
end
|
||||
|
||||
---Get the root of a cabal multi-package project for a path
|
||||
HtProjectHelpers.match_cabal_multi_project_root = root_pattern('cabal.project')
|
||||
|
||||
---Get the root of a cabal package for a path
|
||||
HtProjectHelpers.match_cabal_package_root = root_pattern('*.cabal')
|
||||
|
||||
---Get the root of the cabal project for a path
|
||||
---@param path string File path
|
||||
HtProjectHelpers.match_cabal_project_root = function(path)
|
||||
return HtProjectHelpers.match_cabal_multi_project_root(path) or HtProjectHelpers.match_cabal_package_root(path)
|
||||
end
|
||||
|
||||
---Get the root of the stack project for a path
|
||||
HtProjectHelpers.match_stack_project_root = root_pattern('stack.yaml')
|
||||
|
||||
---Get the root of the project for a path
|
||||
HtProjectHelpers.match_project_root = root_pattern('cabal.project', 'stack.yaml')
|
||||
|
||||
---Get the root of the package for a path
|
||||
HtProjectHelpers.match_package_root = root_pattern('*.cabal', 'package.yaml')
|
||||
|
||||
---Get the directory containing a haskell-language-server hie.yaml
|
||||
HtProjectHelpers.match_hie_yaml = root_pattern('hie.yaml')
|
||||
|
||||
---Get the package.yaml for a given path
|
||||
---@param path string
|
||||
---@return string|nil package_yaml_path
|
||||
function HtProjectHelpers.get_package_yaml(path)
|
||||
local match = root_pattern('package.yaml')
|
||||
local dir = match(path)
|
||||
return dir and dir .. '/package.yaml'
|
||||
end
|
||||
|
||||
---Get the *.cabal for a given path
|
||||
---@param path string
|
||||
---@return string|nil cabal_file_path
|
||||
function HtProjectHelpers.get_package_cabal(path)
|
||||
local match = root_pattern('*.cabal')
|
||||
local dir = match(path)
|
||||
if not dir then
|
||||
return nil
|
||||
end
|
||||
dir = escape_glob_wildcards(dir)
|
||||
for _, pattern in ipairs(vim.fn.glob(compat.joinpath(dir, '*.cabal'), true, true)) do
|
||||
if pattern then
|
||||
return pattern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Is `path` part of a cabal project?
|
||||
---@param path string
|
||||
---@return boolean is_cabal_project
|
||||
function HtProjectHelpers.is_cabal_project(path)
|
||||
local get_root = root_pattern('*.cabal', 'cabal.project')
|
||||
if get_root(path) ~= nil then
|
||||
log.debug('Detected cabal project.')
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Is `path` part of a stack project?
|
||||
---@param path string
|
||||
---@return boolean is_stack_project
|
||||
function HtProjectHelpers.is_stack_project(path)
|
||||
if HtProjectHelpers.match_stack_project_root(path) ~= nil then
|
||||
log.debug('Detected stack project.')
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Get the package name for a given path
|
||||
---@param path string
|
||||
---@return string|nil package_name
|
||||
function HtProjectHelpers.get_package_name(path)
|
||||
local package_path = HtProjectHelpers.match_package_root(path)
|
||||
return package_path and vim.fn.fnamemodify(package_path, ':t')
|
||||
end
|
||||
|
||||
---Parse the package paths (absolute) from a project file
|
||||
---@param project_file string project file (cabal.project or stack.yaml)
|
||||
---@return string[] package_paths
|
||||
---@async
|
||||
function HtProjectHelpers.parse_package_paths(project_file)
|
||||
local package_paths = {}
|
||||
local content = OS.read_file_async(project_file)
|
||||
if not content then
|
||||
return package_paths
|
||||
end
|
||||
local project_dir = vim.fn.fnamemodify(project_file, ':h')
|
||||
local lines = vim.split(content, '\n') or {}
|
||||
local packages_start = false
|
||||
for _, line in ipairs(lines) do
|
||||
if packages_start then
|
||||
local is_indented = line:match('^%s') ~= nil
|
||||
local is_yaml_list_elem = line:match('^%-') ~= nil
|
||||
if not (is_indented or is_yaml_list_elem) then
|
||||
return package_paths
|
||||
end
|
||||
end
|
||||
if packages_start then
|
||||
local trimmed = Strings.trim(line)
|
||||
local pkg_rel_path = trimmed:match('/(.+)')
|
||||
local pkg_path = compat.joinpath(project_dir, pkg_rel_path)
|
||||
if vim.fn.isdirectory(pkg_path) == 1 then
|
||||
package_paths[#package_paths + 1] = pkg_path
|
||||
end
|
||||
end
|
||||
if line:match('packages:') then
|
||||
packages_start = true
|
||||
end
|
||||
end
|
||||
return package_paths
|
||||
end
|
||||
|
||||
---Parse the DAP entry points from a *.cabal file
|
||||
---@param package_path string Path to a package directory
|
||||
---@return HsEntryPoint[] entry_points
|
||||
---@async
|
||||
function HtProjectHelpers.parse_package_entrypoints(package_path)
|
||||
if HtProjectHelpers.is_cabal_project(package_path) then
|
||||
return cabal.parse_package_entrypoints(package_path)
|
||||
end
|
||||
return stack.parse_package_entrypoints(package_path)
|
||||
end
|
||||
|
||||
---@param project_root string Project root directory
|
||||
---@return HsEntryPoint[]
|
||||
---@async
|
||||
function HtProjectHelpers.parse_project_entrypoints(project_root)
|
||||
local entry_points = {}
|
||||
local project_file = compat.joinpath(project_root, 'cabal.project')
|
||||
if vim.fn.filereadable(project_file) == 1 then
|
||||
for _, package_path in pairs(HtProjectHelpers.parse_package_paths(project_file)) do
|
||||
vim.list_extend(entry_points, cabal.parse_package_entrypoints(package_path))
|
||||
end
|
||||
return entry_points
|
||||
end
|
||||
project_file = compat.joinpath(project_root, 'stack.yaml')
|
||||
if vim.fn.filereadable(project_file) == 1 then
|
||||
for _, package_path in pairs(HtProjectHelpers.parse_package_paths(project_file)) do
|
||||
vim.list_extend(entry_points, stack.parse_package_entrypoints(package_path))
|
||||
end
|
||||
return entry_points
|
||||
end
|
||||
return cabal.parse_package_entrypoints(project_root)
|
||||
end
|
||||
|
||||
---@param bufnr number The buffer number
|
||||
---@return boolean is_cabal_file
|
||||
HtProjectHelpers.is_cabal_file = function(bufnr)
|
||||
local filetype = vim.bo[bufnr].filetype
|
||||
return filetype == 'cabal' or filetype == 'cabalproject'
|
||||
end
|
||||
|
||||
return HtProjectHelpers
|
||||
@ -0,0 +1,174 @@
|
||||
---@mod haskell-tools.project haskell-tools Project module
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local deps = require('haskell-tools.deps')
|
||||
|
||||
---@brief [[
|
||||
--- The following commands are available:
|
||||
---
|
||||
--- * `:HsProjectFile` - Open the project file for the current buffer (cabal.project or stack.yaml).
|
||||
--- * `:HsPackageYaml` - Open the package.yaml file for the current buffer.
|
||||
--- * `:HsPackageCabal` - Open the *.cabal file for the current buffer.
|
||||
---@brief ]]
|
||||
|
||||
---@param callback fun(opts:table<string,any>):nil
|
||||
---@param opts table<string,any>
|
||||
local function telescope_package_search(callback, opts)
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
if vim.fn.filewritable(file) == 0 then
|
||||
local err_msg = 'Telescope package search: File not found: ' .. file
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local package_root = HtProjectHelpers.match_package_root(file)
|
||||
if not package_root then
|
||||
local err_msg = 'Telescope package search: No package root found for file ' .. file
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
opts = vim.tbl_deep_extend('keep', {
|
||||
cwd = package_root,
|
||||
prompt_title = (opts.prompt_title_prefix or 'Package') .. ': ' .. vim.fn.fnamemodify(package_root, ':t'),
|
||||
}, opts or {})
|
||||
callback(opts)
|
||||
end
|
||||
|
||||
log.debug('Setting up project tools...')
|
||||
|
||||
--- Live grep the current package with telescope.
|
||||
--- available if nvim-telescope/telescope.nvim is installed.
|
||||
---@param opts table|nil telescope options
|
||||
local function telescope_package_grep(opts)
|
||||
local t = require('telescope.builtin')
|
||||
opts = vim.tbl_deep_extend('keep', { prompt_title_prefix = 'package live grep' }, opts or {})
|
||||
telescope_package_search(t.live_grep, opts)
|
||||
end
|
||||
|
||||
--- Find file in the current package with telescope
|
||||
--- available if nvim-telescope/telescope.nvim is installed.
|
||||
---@param opts table|nil telescope options
|
||||
local function telescope_package_files(opts)
|
||||
local t = require('telescope.builtin')
|
||||
opts = vim.tbl_deep_extend('keep', { prompt_title_prefix = 'package file search' }, opts or {})
|
||||
telescope_package_search(t.find_files, opts)
|
||||
end
|
||||
|
||||
---@class HsProjectTools
|
||||
local HsProjectTools = {}
|
||||
|
||||
---Get the project's root directory
|
||||
---@param project_file string The path to a project file
|
||||
---@return string|nil
|
||||
HsProjectTools.root_dir = function(project_file)
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
return HtProjectHelpers.match_cabal_project_root(project_file)
|
||||
or HtProjectHelpers.match_stack_project_root(project_file)
|
||||
or HtProjectHelpers.match_package_root(project_file)
|
||||
or HtProjectHelpers.match_hie_yaml(project_file)
|
||||
end
|
||||
|
||||
---Open the package.yaml of the package containing the current buffer.
|
||||
---@return nil
|
||||
HsProjectTools.open_package_yaml = function()
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
vim.schedule(function()
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
local result = HtProjectHelpers.get_package_yaml(file)
|
||||
if not result then
|
||||
local context = ''
|
||||
if HtProjectHelpers.is_cabal_project(file) then
|
||||
context = ' cabal project file'
|
||||
end
|
||||
local err_msg = 'HsPackageYaml: Cannot find package.yaml file for' .. context .. ': ' .. file
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd('e ' .. result)
|
||||
end)
|
||||
end
|
||||
|
||||
---Open the *.cabal file of the package containing the current buffer.
|
||||
---@return nil
|
||||
HsProjectTools.open_package_cabal = function()
|
||||
vim.schedule(function()
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
if vim.fn.filewritable(file) ~= 0 and not HtProjectHelpers.is_cabal_project(file) then
|
||||
vim.notify('HsPackageCabal: Not a cabal project?', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local result = HtProjectHelpers.get_package_cabal(file)
|
||||
if not result then
|
||||
local err_msg = 'HsPackageCabal: Cannot find *.cabal file for: ' .. file
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd('e ' .. result)
|
||||
end)
|
||||
end
|
||||
|
||||
---Open the current buffer's project file (cabal.project or stack.yaml).
|
||||
---@return nil
|
||||
HsProjectTools.open_project_file = function()
|
||||
vim.schedule(function()
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
local stack_project_root = HtProjectHelpers.match_stack_project_root(file)
|
||||
if stack_project_root then
|
||||
vim.cmd('e ' .. stack_project_root .. '/stack.yaml')
|
||||
return
|
||||
end
|
||||
local cabal_project_root = HtProjectHelpers.match_cabal_multi_project_root(file)
|
||||
if cabal_project_root then
|
||||
vim.cmd('e ' .. cabal_project_root .. '/cabal.project')
|
||||
return
|
||||
end
|
||||
local package_cabal = HtProjectHelpers.get_package_cabal(file)
|
||||
if package_cabal then
|
||||
vim.cmd('e ' .. package_cabal)
|
||||
end
|
||||
local err_msg = 'HsProjectFile: Cannot find project file from: ' .. file
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
end)
|
||||
end
|
||||
|
||||
HsProjectTools.telescope_package_grep = deps.has('telescope.builtin') and telescope_package_grep or nil
|
||||
|
||||
HsProjectTools.telescope_package_files = deps.has('telescope.builtin') and telescope_package_files or nil
|
||||
|
||||
local commands = {
|
||||
{
|
||||
'HsPackageYaml',
|
||||
function()
|
||||
HsProjectTools.open_package_yaml()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HsPackageCabal',
|
||||
function()
|
||||
HsProjectTools.open_package_cabal()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HsProjectFile',
|
||||
function()
|
||||
HsProjectTools.open_project_file()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
}
|
||||
|
||||
--- Available if nvim-telescope/telescope.nvim is installed.
|
||||
for _, command in ipairs(commands) do
|
||||
vim.api.nvim_create_user_command(unpack(command))
|
||||
end
|
||||
|
||||
return HsProjectTools
|
||||
@ -0,0 +1,159 @@
|
||||
---@mod haskell-tools.stack
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Helper functions related to stack projects
|
||||
---@brief ]]
|
||||
|
||||
local Strings = require('haskell-tools.strings')
|
||||
local HtParser = require('haskell-tools.parser')
|
||||
local Dap = require('haskell-tools.dap.internal')
|
||||
local OS = require('haskell-tools.os')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---@class StackProjectHelper
|
||||
local StackProjectHelper = {}
|
||||
|
||||
---@param str string
|
||||
---@return boolean is_yaml_comment
|
||||
local function is_yaml_comment(str)
|
||||
return vim.startswith(Strings.trim(str), '#')
|
||||
end
|
||||
|
||||
---@class StackEntryPointParserData
|
||||
---@field idx integer
|
||||
---@field lines string[]
|
||||
---@field line string
|
||||
---@field package_dir string
|
||||
---@field next_line string|nil
|
||||
|
||||
---@class StackEntryPointParserState
|
||||
---@field package_name string
|
||||
---@field entry_points HsEntryPoint[]
|
||||
---@field mains string[]
|
||||
---@field source_dirs string[]
|
||||
---@field parsing_exe_list boolean
|
||||
---@field parsing_exe boolean
|
||||
---@field parsing_source_dirs boolean
|
||||
---@field exe_indent integer | nil
|
||||
---@field exe_name string | nil
|
||||
|
||||
---@param data StackEntryPointParserData
|
||||
---@param state StackEntryPointParserState
|
||||
local function parse_exe_list_line(data, state)
|
||||
local package_dir = data.package_dir
|
||||
local idx = data.idx
|
||||
local lines = data.lines
|
||||
local line = data.line
|
||||
local next_line = lines[idx + 1]
|
||||
local indent = HtParser.get_indent(line)
|
||||
state.exe_indent = state.exe_indent or indent
|
||||
state.exe_name = indent == state.exe_indent and line:match('%s*(.+):') or state.exe_name
|
||||
if state.parsing_exe then
|
||||
local main = line:match('main:%s+(.+)%.hs')
|
||||
if not main and line:match('main:') and next_line then
|
||||
main = next_line:match('%s+(.+)%.hs')
|
||||
end
|
||||
if main then
|
||||
table.insert(state.mains, main .. '.hs')
|
||||
end
|
||||
local source_dir = line:match('source%-dirs:%s+(.+)')
|
||||
if source_dir then
|
||||
-- Single source directory
|
||||
state.parsing_source_dirs = false
|
||||
end
|
||||
if state.parsing_source_dirs then
|
||||
source_dir = line:match('%s+%-%s*(.*)') or line:match('%s+(.*)')
|
||||
end
|
||||
if source_dir then
|
||||
table.insert(state.source_dirs, source_dir)
|
||||
end
|
||||
local is_source_dir_list = not source_dir and line:match('source%-dirs:') ~= nil
|
||||
state.parsing_source_dirs = is_source_dir_list
|
||||
or (
|
||||
state.parsing_source_dirs
|
||||
and next_line
|
||||
and (next_line:match('^%s+%-') or HtParser.get_indent(next_line) > indent)
|
||||
)
|
||||
end
|
||||
if state.parsing_exe and (not next_line or HtParser.get_indent(next_line) == 0 or indent <= state.exe_indent) then
|
||||
vim.list_extend(
|
||||
state.entry_points,
|
||||
Dap.mk_entry_points(state.package_name, state.exe_name, package_dir, state.mains, state.source_dirs)
|
||||
)
|
||||
state.mains = {}
|
||||
state.source_dirs = {}
|
||||
state.parsing_exe = false
|
||||
else
|
||||
state.parsing_exe = indent >= state.exe_indent
|
||||
end
|
||||
end
|
||||
|
||||
---@param data StackEntryPointParserData
|
||||
---@param state StackEntryPointParserState
|
||||
local function get_entrypoint_from_line(data, state)
|
||||
local line = data.line
|
||||
state.package_name = state.package_name or line:match('^name:%s*(.+)')
|
||||
local indent = HtParser.get_indent(line)
|
||||
if indent == 0 then
|
||||
state.parsing_exe_list = false
|
||||
state.exe_indent = nil
|
||||
end
|
||||
if state.parsing_exe_list then
|
||||
parse_exe_list_line(data, state)
|
||||
end
|
||||
end
|
||||
|
||||
---Parse the DAP entry points from a *.cabal file
|
||||
---@param package_file string Path to the *.cabal file
|
||||
---@return HsEntryPoint[] entry_points
|
||||
---@async
|
||||
local function parse_package_entrypoints(package_file)
|
||||
local state = {
|
||||
entry_points = {},
|
||||
mains = {},
|
||||
source_dirs = {},
|
||||
parsing_exe_list = false,
|
||||
parsing_exe = false,
|
||||
parsing_source_dirs = false,
|
||||
}
|
||||
local package_dir = vim.fn.fnamemodify(package_file, ':h') or package_file
|
||||
local content = OS.read_file_async(package_file)
|
||||
if not content then
|
||||
return state.entry_points
|
||||
end
|
||||
local lines = vim.split(content, '\n') or {}
|
||||
for idx, line in ipairs(lines) do
|
||||
if not is_yaml_comment(line) then
|
||||
---@type StackEntryPointParserData
|
||||
local data = {
|
||||
package_dir = package_dir,
|
||||
line = line,
|
||||
lines = lines,
|
||||
idx = idx,
|
||||
}
|
||||
get_entrypoint_from_line(data, state)
|
||||
end
|
||||
if line:match('^executables:') or line:match('^tests:') then
|
||||
state.parsing_exe_list = true
|
||||
end
|
||||
end
|
||||
return state.entry_points
|
||||
end
|
||||
|
||||
---Parse the DAP entry points from a package.yaml file
|
||||
---@param package_path string Path to a package directory
|
||||
---@return HsEntryPoint[] entry_points
|
||||
---@async
|
||||
function StackProjectHelper.parse_package_entrypoints(package_path)
|
||||
local entry_points = {}
|
||||
for _, package_file in pairs(vim.fn.glob(compat.joinpath(package_path, 'package.yaml'), true, true)) do
|
||||
vim.list_extend(entry_points, parse_package_entrypoints(package_file))
|
||||
end
|
||||
return entry_points
|
||||
end
|
||||
|
||||
return StackProjectHelper
|
||||
@ -0,0 +1,279 @@
|
||||
---@mod haskell-tools.repl.builtin
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---Utility functions for the ghci repl module.
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
|
||||
---@class BuiltinRepl
|
||||
---@field bufnr number
|
||||
---@field job_id number
|
||||
---@field cmd string[]
|
||||
|
||||
---@type BuiltinRepl | nil
|
||||
local BuiltinRepl = nil
|
||||
|
||||
local function is_repl_loaded()
|
||||
return BuiltinRepl ~= nil and vim.api.nvim_buf_is_loaded(BuiltinRepl.bufnr)
|
||||
end
|
||||
|
||||
---@param callback fun(repl:BuiltinRepl)
|
||||
---@return nil
|
||||
local function when_repl_loaded(callback)
|
||||
if is_repl_loaded() then
|
||||
local repl = BuiltinRepl
|
||||
---@cast repl BuiltinRepl
|
||||
callback(repl)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param cmd string[]?
|
||||
local function is_new_cmd(cmd)
|
||||
return BuiltinRepl ~= nil and table.concat(BuiltinRepl.cmd) ~= table.concat(cmd or {})
|
||||
end
|
||||
|
||||
---Creates a repl on buffer with id `bufnr`.
|
||||
---@param bufnr number Buffer to be used.
|
||||
---@param cmd string[] command to start the repl
|
||||
---@param opts ReplViewOpts?
|
||||
---@return nil
|
||||
local function buf_create_repl(bufnr, cmd, opts)
|
||||
vim.api.nvim_win_set_buf(0, bufnr)
|
||||
opts = vim.tbl_extend('force', vim.empty_dict(), opts or {})
|
||||
local function delete_repl_buf()
|
||||
local winid = vim.fn.bufwinid(bufnr)
|
||||
if winid ~= nil then
|
||||
vim.api.nvim_win_close(winid, true)
|
||||
end
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end
|
||||
if opts.delete_buffer_on_exit then
|
||||
opts.on_exit = function(_, exit_code, _)
|
||||
log.debug('repl.builtin: exit')
|
||||
if exit_code ~= 0 then
|
||||
local msg = 'repl.builtin: non-zero exit code: ' .. exit_code
|
||||
log.warn(msg)
|
||||
vim.notify(msg, vim.log.levels.WARN)
|
||||
end
|
||||
delete_repl_buf()
|
||||
end
|
||||
local repl_log = function(logger)
|
||||
return function(_, data, name)
|
||||
logger { 'repl.builtin', data, name }
|
||||
end
|
||||
end
|
||||
opts.on_stdout = repl_log(log.debug)
|
||||
opts.on_stderr = repl_log(log.warn)
|
||||
opts.on_stdin = repl_log(log.debug)
|
||||
end
|
||||
log.debug { 'repl.builtin: Opening terminal', cmd, opts }
|
||||
local job_id = vim.fn.termopen(cmd, opts)
|
||||
if not job_id then
|
||||
log.error('repl.builtin: Failed to open a terminal')
|
||||
vim.notify('haskell-tools: Could not start the repl.', vim.log.levels.ERROR)
|
||||
delete_repl_buf()
|
||||
return
|
||||
end
|
||||
BuiltinRepl = {
|
||||
bufnr = bufnr,
|
||||
job_id = job_id,
|
||||
cmd = cmd,
|
||||
}
|
||||
log.debug { 'repl.builtin: Created repl.', BuiltinRepl }
|
||||
end
|
||||
|
||||
---Create a split
|
||||
---@param size function|number|nil
|
||||
local function create_split(size)
|
||||
size = size and (type(size) == 'function' and size() or size) or vim.o.lines / 3
|
||||
local args = vim.empty_dict() or {}
|
||||
table.insert(args, size)
|
||||
table.insert(args, 'split')
|
||||
vim.cmd(table.concat(args, ' '))
|
||||
end
|
||||
|
||||
---Create a vertical split
|
||||
---@param size function|number?
|
||||
local function create_vsplit(size)
|
||||
size = size and (type(size) == 'function' and size() or size) or vim.o.columns / 2
|
||||
local args = vim.empty_dict() or {}
|
||||
table.insert(args, size)
|
||||
table.insert(args, 'vsplit')
|
||||
vim.cmd(table.concat(args, ' '))
|
||||
end
|
||||
|
||||
---Create a new tab
|
||||
---@param _ any
|
||||
local function create_tab(_)
|
||||
vim.cmd('tabnew')
|
||||
end
|
||||
|
||||
---@param mk_repl_cmd fun(string):(string[]?)
|
||||
---@param options ReplConfig
|
||||
---@return ReplHandlerImpl handler
|
||||
return function(mk_repl_cmd, options)
|
||||
---@class ReplHandlerImpl
|
||||
local ReplHandlerImpl = {}
|
||||
|
||||
---Create a new repl (or toggle its visibility)
|
||||
---@param create_win function|number Function for creating the window or an existing window number
|
||||
---@param mk_cmd fun():string[] Function for creating the repl command
|
||||
---@param opts ReplViewOpts?
|
||||
---@return nil
|
||||
local function create_or_toggle(create_win, mk_cmd, opts)
|
||||
local cmd = mk_cmd()
|
||||
if cmd == nil then
|
||||
local err_msg = 'haskell-tools.repl.builtin: Could not create a repl command.'
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if is_new_cmd(cmd) then
|
||||
log.debug { 'repl.builtin: New command', cmd }
|
||||
ReplHandlerImpl.quit()
|
||||
end
|
||||
if is_repl_loaded() then
|
||||
local repl = BuiltinRepl
|
||||
---@cast repl BuiltinRepl
|
||||
log.debug('repl.builtin: is loaded')
|
||||
local winid = vim.fn.bufwinid(repl.bufnr)
|
||||
if winid ~= -1 then
|
||||
log.debug('repl.builtin: Hiding window ' .. winid)
|
||||
vim.api.nvim_win_hide(winid)
|
||||
else
|
||||
create_win()
|
||||
vim.api.nvim_set_current_buf(repl.bufnr)
|
||||
winid = vim.fn.bufwinid(repl.bufnr)
|
||||
if winid ~= nil then
|
||||
log.debug('repl.builtin: Created window ' .. winid)
|
||||
vim.api.nvim_set_current_win(winid)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
log.debug('repl.builtin: is not loaded')
|
||||
opts = opts or vim.empty_dict()
|
||||
local bufnr = vim.api.nvim_create_buf(true, true)
|
||||
create_win()
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
local winid = vim.fn.bufwinid(bufnr)
|
||||
if winid ~= nil then
|
||||
log.debug('repl.builtin: Created window ' .. winid)
|
||||
vim.api.nvim_set_current_win(winid)
|
||||
end
|
||||
buf_create_repl(bufnr, cmd, opts)
|
||||
end
|
||||
|
||||
---@type ReplView
|
||||
local ReplView = {
|
||||
---Create a new repl in a horizontal split
|
||||
---@param opts ReplViewOpts?
|
||||
---@return fun(mk_cmd_fun) create_repl
|
||||
create_repl_split = function(opts)
|
||||
return function(mk_cmd)
|
||||
create_or_toggle(create_split, mk_cmd, opts)
|
||||
end
|
||||
end,
|
||||
|
||||
---Create a new repl in a vertical split
|
||||
---@param opts ReplViewOpts?
|
||||
---@return fun(function) create_repl
|
||||
create_repl_vsplit = function(opts)
|
||||
return function(mk_cmd)
|
||||
create_or_toggle(create_vsplit, mk_cmd, opts)
|
||||
end
|
||||
end,
|
||||
|
||||
---Create a new repl in a new tab
|
||||
---@param opts ReplViewOpts?
|
||||
---@return fun(function) create_repl
|
||||
create_repl_tabnew = function(opts)
|
||||
return function(mk_cmd)
|
||||
create_or_toggle(create_tab, mk_cmd, opts)
|
||||
end
|
||||
end,
|
||||
|
||||
---Create a new repl in the current window
|
||||
---@param opts ReplViewOpts?
|
||||
---@return fun(function) create_repl
|
||||
create_repl_cur_win = function(opts)
|
||||
return function(mk_cmd)
|
||||
create_or_toggle(function(_) end, mk_cmd, opts)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
log.debug { 'repl.builtin setup', options }
|
||||
---@private
|
||||
ReplHandlerImpl.go_back = options.auto_focus ~= true
|
||||
|
||||
---@param filepath string path of the file to load into the repl
|
||||
---@param _ table?
|
||||
function ReplHandlerImpl.toggle(filepath, _)
|
||||
local cur_win = vim.api.nvim_get_current_win()
|
||||
if filepath and not vim.endswith(filepath, '.hs') then
|
||||
local err_msg = 'haskell-tools.repl.builtin: Not a Haskell file: ' .. filepath
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
---@return string[]?
|
||||
local function mk_repl_cmd_wrapped()
|
||||
return mk_repl_cmd(filepath)
|
||||
end
|
||||
|
||||
local create_or_toggle_callback = options.builtin.create_repl_window(ReplView)
|
||||
create_or_toggle_callback(mk_repl_cmd_wrapped)
|
||||
if cur_win ~= -1 and ReplHandlerImpl.go_back then
|
||||
vim.api.nvim_set_current_win(cur_win)
|
||||
else
|
||||
vim.cmd('startinsert')
|
||||
end
|
||||
end
|
||||
|
||||
---Quit the repl
|
||||
---@return nil
|
||||
function ReplHandlerImpl.quit()
|
||||
when_repl_loaded(function(repl)
|
||||
log.debug('repl.builtin: sending quit to repl.')
|
||||
local success, result = pcall(ReplHandlerImpl.send_cmd, ':q')
|
||||
if not success then
|
||||
log.warn { 'repl.builtin: Could not send quit command', result }
|
||||
end
|
||||
local winid = vim.fn.bufwinid(repl.bufnr)
|
||||
if winid ~= -1 then
|
||||
vim.api.nvim_win_close(winid, true)
|
||||
end
|
||||
vim.api.nvim_buf_delete(repl.bufnr, { force = true })
|
||||
end)
|
||||
end
|
||||
|
||||
---Send a command to the repl, followed by <cr>
|
||||
---@param txt string The text to send
|
||||
---@return nil
|
||||
function ReplHandlerImpl.send_cmd(txt)
|
||||
when_repl_loaded(function(repl)
|
||||
local cr = '\13'
|
||||
local repl_winid = vim.fn.bufwinid(repl.bufnr)
|
||||
local function repl_set_cursor()
|
||||
if repl_winid ~= -1 then
|
||||
vim.api.nvim_win_set_cursor(repl_winid, { vim.api.nvim_buf_line_count(repl.bufnr), 0 })
|
||||
end
|
||||
end
|
||||
repl_set_cursor()
|
||||
vim.api.nvim_chan_send(repl.job_id, txt .. cr)
|
||||
repl_set_cursor()
|
||||
if not ReplHandlerImpl.go_back and repl_winid ~= nil then
|
||||
vim.api.nvim_set_current_win(repl_winid)
|
||||
vim.cmd('startinsert')
|
||||
end
|
||||
end)
|
||||
end
|
||||
return ReplHandlerImpl
|
||||
end
|
||||
@ -0,0 +1,291 @@
|
||||
---@mod haskell-tools.repl haskell-tools GHCi REPL module
|
||||
|
||||
---@bruief [[
|
||||
---Tools for interaction with a GHCi REPL
|
||||
---@bruief ]]
|
||||
|
||||
---@brief [[
|
||||
--- The following commands are available:
|
||||
---
|
||||
--- * `:HtReplToggle` - Toggle a GHCi repl.
|
||||
--- * `:HtReplQuit` - Quit the current repl.
|
||||
--- * `:HtReplLoad` - Load a Haskell file into the repl.
|
||||
--- * `:HtReplReload` - Reload the current repl.
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
---Extend a repl command for `file`.
|
||||
---If `file` is `nil`, create a repl the nearest package.
|
||||
---@param cmd string[] The command to extend
|
||||
---@param file string|nil An optional project file
|
||||
---@return string[]|nil
|
||||
local function extend_repl_cmd(cmd, file)
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
if file == nil then
|
||||
file = vim.api.nvim_buf_get_name(0)
|
||||
log.debug('extend_repl_cmd: No file specified. Using current buffer: ' .. file)
|
||||
local project_root = HtProjectHelpers.match_project_root(file)
|
||||
local subpackage = project_root and HtProjectHelpers.get_package_name(file)
|
||||
if subpackage then
|
||||
table.insert(cmd, subpackage)
|
||||
log.debug { 'extend_repl_cmd: Extended cmd with package.', cmd }
|
||||
return cmd
|
||||
else
|
||||
log.debug { 'extend_repl_cmd: No subpackage or no package found.', cmd }
|
||||
return cmd
|
||||
end
|
||||
end
|
||||
log.debug('extend_repl_cmd: File: ' .. file)
|
||||
local project_root = HtProjectHelpers.match_project_root(file)
|
||||
local subpackage = project_root and HtProjectHelpers.get_package_name(file)
|
||||
if not subpackage then
|
||||
log.debug { 'extend_repl_cmd: No package found.', cmd }
|
||||
return cmd
|
||||
end
|
||||
if vim.endswith(file, '.hs') then
|
||||
table.insert(cmd, file)
|
||||
else
|
||||
log.debug('extend_repl_cmd: Not a Haskell file.')
|
||||
table.insert(cmd, subpackage)
|
||||
end
|
||||
log.debug { 'extend_repl_cmd', cmd }
|
||||
return cmd
|
||||
end
|
||||
|
||||
---Create a cabal repl command for `file`.
|
||||
---If `file` is `nil`, create a repl the nearest package.
|
||||
---@param file string|nil
|
||||
---@return string[]|nil command
|
||||
local function mk_cabal_repl_cmd(file)
|
||||
return extend_repl_cmd({ 'cabal', 'repl', '--ghc-option', '-Wwarn' }, file)
|
||||
end
|
||||
|
||||
---Create a stack repl command for `file`.
|
||||
---If `file` is `nil`, create a repl the nearest package.
|
||||
---@param file string|nil
|
||||
---@return string[]|nil command
|
||||
local function mk_stack_repl_cmd(file)
|
||||
return extend_repl_cmd({ 'stack', 'ghci' }, file)
|
||||
end
|
||||
|
||||
---Create the command to create a repl for a file.
|
||||
---If `file` is `nil`, create a repl for the nearest package.
|
||||
---@param file string|nil
|
||||
---@return table|nil command
|
||||
local function mk_repl_cmd(file)
|
||||
local chk_path = file
|
||||
if not chk_path then
|
||||
chk_path = vim.api.nvim_buf_get_name(0)
|
||||
if vim.fn.filewritable(chk_path) == 0 then
|
||||
local err_msg = 'haskell-tools.repl: File not found. Has it been saved? ' .. chk_path
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local opts = HTConfig.tools.repl
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
if Types.evaluate(opts.prefer) == 'stack' and HtProjectHelpers.is_stack_project(chk_path) then
|
||||
return mk_stack_repl_cmd(file)
|
||||
end
|
||||
if HtProjectHelpers.is_cabal_project(chk_path) then
|
||||
return mk_cabal_repl_cmd(file)
|
||||
end
|
||||
if HtProjectHelpers.is_stack_project(chk_path) then
|
||||
return mk_stack_repl_cmd(file)
|
||||
end
|
||||
if vim.fn.executable('ghci') == 1 then
|
||||
local cmd = compat.tbl_flatten { 'ghci', file and { file } or {} }
|
||||
log.debug { 'mk_repl_cmd', cmd }
|
||||
return cmd
|
||||
end
|
||||
local err_msg = 'haskell-tools.repl: No ghci executable found.'
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return nil
|
||||
end
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local opts = HTConfig.tools.repl
|
||||
---@type ReplHandlerImpl
|
||||
local handler
|
||||
|
||||
local handler_type = Types.evaluate(opts.handler)
|
||||
---@cast handler_type ReplHandler
|
||||
if handler_type == 'toggleterm' then
|
||||
log.info('handler = toggleterm')
|
||||
handler = require('haskell-tools.repl.toggleterm')(mk_repl_cmd, opts)
|
||||
else
|
||||
if handler_type ~= 'builtin' then
|
||||
log.warn('Invalid repl handler type. Falling back to builtin')
|
||||
vim.notify_once(
|
||||
'haskell-tools.repl: the handler "' .. handler_type .. '" is invalid. Defaulting to "builtin".',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
else
|
||||
log.info('handler = builtin')
|
||||
end
|
||||
handler = require('haskell-tools.repl.builtin')(mk_repl_cmd, opts)
|
||||
end
|
||||
|
||||
local function handle_reg(cmd, reg)
|
||||
local data = vim.fn.getreg(reg or '"')
|
||||
handler.send_cmd(cmd .. ' ' .. data)
|
||||
end
|
||||
|
||||
local function handle_cword(cmd)
|
||||
local cword = vim.fn.expand('<cword>')
|
||||
handler.send_cmd(cmd .. ' ' .. cword)
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
local function repl_send_lines(lines)
|
||||
if #lines > 1 then
|
||||
handler.send_cmd(':{')
|
||||
for _, line in ipairs(lines) do
|
||||
handler.send_cmd(line)
|
||||
end
|
||||
handler.send_cmd(':}')
|
||||
else
|
||||
handler.send_cmd(lines[1])
|
||||
end
|
||||
end
|
||||
|
||||
---@class HsReplTools
|
||||
local HsReplTools = {}
|
||||
|
||||
HsReplTools.mk_repl_cmd = mk_repl_cmd
|
||||
|
||||
---Create the command to create a repl for the current buffer.
|
||||
---@return table|nil command
|
||||
HsReplTools.buf_mk_repl_cmd = function()
|
||||
local file = vim.api.nvim_buf_get_name(0)
|
||||
return mk_repl_cmd(file)
|
||||
end
|
||||
|
||||
---Toggle a GHCi REPL
|
||||
HsReplTools.toggle = handler.toggle
|
||||
|
||||
---Quit the REPL
|
||||
HsReplTools.quit = handler.quit
|
||||
|
||||
---Can be used to send text objects to the repl.
|
||||
---@usage [[
|
||||
---vim.keymap.set('n', 'ghc', ht.repl.operator, {noremap = true})
|
||||
---@usage ]]
|
||||
---@see operatorfunc
|
||||
HsReplTools.operator = function()
|
||||
local old_operator_func = vim.go.operatorfunc
|
||||
_G.op_func_send_to_repl = function()
|
||||
local start = vim.api.nvim_buf_get_mark(0, '[')
|
||||
local finish = vim.api.nvim_buf_get_mark(0, ']')
|
||||
local text = vim.api.nvim_buf_get_text(0, start[1] - 1, start[2], finish[1], finish[2] + 1, {})
|
||||
repl_send_lines(text)
|
||||
vim.go.operatorfunc = old_operator_func
|
||||
_G.op_func_formatting = nil
|
||||
end
|
||||
vim.go.operatorfunc = 'v:lua.op_func_send_to_repl'
|
||||
vim.api.nvim_feedkeys('g@', 'n', false)
|
||||
end
|
||||
|
||||
---Paste from register `reg` to the REPL
|
||||
---@param reg string|nil register (defaults to '"')
|
||||
HsReplTools.paste = function(reg)
|
||||
local data = vim.fn.getreg(reg or '"')
|
||||
---@cast data string
|
||||
if vim.endswith(data, '\n') then
|
||||
data = data:sub(1, #data - 1)
|
||||
end
|
||||
local lines = vim.split(data, '\n')
|
||||
if #lines <= 1 then
|
||||
lines = { data }
|
||||
end
|
||||
repl_send_lines(lines)
|
||||
end
|
||||
|
||||
---Query the REPL for the type of register `reg`
|
||||
---@param reg string|nil register (defaults to '"')
|
||||
HsReplTools.paste_type = function(reg)
|
||||
handle_reg(':t', reg)
|
||||
end
|
||||
|
||||
---Query the REPL for the type of word under the cursor
|
||||
HsReplTools.cword_type = function()
|
||||
handle_cword(':t')
|
||||
end
|
||||
|
||||
---Query the REPL for info on register `reg`
|
||||
---@param reg string|nil register (defaults to '"')
|
||||
HsReplTools.paste_info = function(reg)
|
||||
handle_reg(':i', reg)
|
||||
end
|
||||
|
||||
---Query the REPL for the type of word under the cursor
|
||||
HsReplTools.cword_info = function()
|
||||
handle_cword(':i')
|
||||
end
|
||||
|
||||
---Load a file into the REPL
|
||||
---@param filepath string The absolute file path
|
||||
HsReplTools.load_file = function(filepath)
|
||||
if vim.fn.filereadable(filepath) == 0 then
|
||||
local err_msg = 'File: ' .. filepath .. ' does not exist or is not readable.'
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
end
|
||||
handler.send_cmd(':l ' .. filepath)
|
||||
end
|
||||
|
||||
---Reload the repl
|
||||
HsReplTools.reload = function()
|
||||
handler.send_cmd(':r')
|
||||
end
|
||||
|
||||
vim.keymap.set('n', 'ghc', HsReplTools.operator, { noremap = true })
|
||||
|
||||
local commands = {
|
||||
{
|
||||
'HtReplToggle',
|
||||
---@param tbl table
|
||||
function(tbl)
|
||||
local filepath = tbl.args ~= '' and vim.fn.expand(tbl.args)
|
||||
---@cast filepath string
|
||||
HsReplTools.toggle(filepath)
|
||||
end,
|
||||
{ nargs = '?' },
|
||||
},
|
||||
{
|
||||
'HtReplLoad',
|
||||
---@param tbl table
|
||||
function(tbl)
|
||||
local filepath = vim.fn.expand(tbl.args)
|
||||
---@cast filepath string
|
||||
HsReplTools.load_file(filepath)
|
||||
end,
|
||||
{ nargs = 1 },
|
||||
},
|
||||
{
|
||||
'HtReplQuit',
|
||||
function()
|
||||
HsReplTools.quit()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
{
|
||||
'HtReplReload',
|
||||
function()
|
||||
HsReplTools.reload()
|
||||
end,
|
||||
{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, command in ipairs(commands) do
|
||||
vim.api.nvim_create_user_command(unpack(command))
|
||||
end
|
||||
|
||||
return HsReplTools
|
||||
@ -0,0 +1,138 @@
|
||||
---@mod haskell-tools.repl.toggleterm
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
---Wraps the toggleterm.nvim API to provide a GHIi repl.
|
||||
|
||||
---@brief ]]
|
||||
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local deps = require('haskell-tools.deps')
|
||||
|
||||
local last_cmd = ''
|
||||
|
||||
---@param cmd string?
|
||||
local function is_new_cmd(cmd)
|
||||
return last_cmd ~= (cmd or '')
|
||||
end
|
||||
|
||||
--- Quote a string
|
||||
--- @param str string
|
||||
--- @return string quoted_string
|
||||
local function quote(str)
|
||||
return '"' .. str .. '"'
|
||||
end
|
||||
|
||||
---@param mk_repl_cmd fun(string?):string[]? Function for building the repl that takes an optional file path
|
||||
---@param opts ReplConfig
|
||||
---@return ReplHandlerImpl
|
||||
return function(mk_repl_cmd, opts)
|
||||
local ReplHandlerImpl = {
|
||||
---@private
|
||||
---@type Terminal | nil
|
||||
terminal = nil,
|
||||
}
|
||||
opts = opts or vim.empty_dict()
|
||||
if opts.auto_focus == nil then
|
||||
---@private
|
||||
ReplHandlerImpl.go_back = true
|
||||
else
|
||||
---@private
|
||||
ReplHandlerImpl.go_back = not opts.auto_focus
|
||||
end
|
||||
log.debug('repl.toggleterm setup')
|
||||
---@type Terminal
|
||||
local Terminal = deps.require_toggleterm('toggleterm.terminal').Terminal
|
||||
|
||||
---@param cmd string The command to execute in the terminal
|
||||
---@return Terminal
|
||||
local function mk_new_terminal(cmd)
|
||||
local terminal_opts = {
|
||||
cmd = cmd,
|
||||
hidden = true,
|
||||
close_on_exit = false,
|
||||
on_stdout = function(_, job, data, name)
|
||||
log.debug { 'Job ' .. job .. ' - stdout', data, name }
|
||||
end,
|
||||
on_stderr = function(_, job, data, name)
|
||||
log.warn { 'Job ' .. job .. ' - stderr', data, name }
|
||||
end,
|
||||
on_exit = function(_, job, exit_code, name)
|
||||
log.debug { 'Job ' .. job .. ' - exit code ' .. exit_code, name }
|
||||
end,
|
||||
}
|
||||
log.debug { 'Creating new terminal', terminal_opts }
|
||||
return Terminal:new(terminal_opts)
|
||||
end
|
||||
|
||||
--- @param filepath string? Path of the file to load into the repl
|
||||
function ReplHandlerImpl.toggle(filepath, _)
|
||||
opts = opts or vim.empty_dict()
|
||||
if filepath and not vim.endswith(filepath, '.hs') then
|
||||
local err_msg = 'haskell-tools.repl.toggleterm: Not a Haskell file: ' .. filepath
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local cmd = mk_repl_cmd(filepath and quote(filepath)) or {}
|
||||
if #cmd == 0 then
|
||||
local err_msg = 'haskell-tools.repl.toggleterm: Could not create a repl command.'
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
if is_new_cmd(cmd_str) then
|
||||
log.debug { 'repl.toggleterm: New command', cmd_str }
|
||||
ReplHandlerImpl.quit()
|
||||
end
|
||||
local cur_win = vim.api.nvim_get_current_win()
|
||||
local is_normal_mode = vim.api.nvim_get_mode().mode == 'n'
|
||||
ReplHandlerImpl.terminal = ReplHandlerImpl.terminal or mk_new_terminal(cmd_str)
|
||||
local function toggle()
|
||||
ReplHandlerImpl.terminal:toggle()
|
||||
end
|
||||
local success, result = pcall(toggle)
|
||||
if not success then
|
||||
log.error { 'repl.toggleterm: toggle failed', result }
|
||||
end
|
||||
if cur_win ~= -1 and ReplHandlerImpl.go_back then
|
||||
vim.api.nvim_set_current_win(cur_win)
|
||||
if is_normal_mode then
|
||||
vim.cmd('stopinsert')
|
||||
end
|
||||
end
|
||||
last_cmd = cmd_str
|
||||
end
|
||||
|
||||
---Quit the repl
|
||||
---@retrun nil
|
||||
function ReplHandlerImpl.quit()
|
||||
if ReplHandlerImpl.terminal ~= nil then
|
||||
log.debug('repl.toggleterm: sending quit to repl.')
|
||||
local success, result = pcall(ReplHandlerImpl.send_cmd, ':q')
|
||||
if not success then
|
||||
log.warn { 'repl.toggleterm: Could not send quit command', result }
|
||||
end
|
||||
ReplHandlerImpl.terminal:close()
|
||||
ReplHandlerImpl.terminal = nil
|
||||
end
|
||||
end
|
||||
|
||||
---Send a command to the repl, followed by <cr>
|
||||
---@param txt string the command text to send
|
||||
---@return nil
|
||||
function ReplHandlerImpl.send_cmd(txt)
|
||||
opts = opts or vim.empty_dict()
|
||||
vim.tbl_extend('force', {
|
||||
go_back = false,
|
||||
}, opts)
|
||||
if ReplHandlerImpl.terminal ~= nil then
|
||||
ReplHandlerImpl.terminal:send(txt, ReplHandlerImpl.go_back)
|
||||
end
|
||||
end
|
||||
return ReplHandlerImpl
|
||||
end
|
||||
@ -0,0 +1,21 @@
|
||||
---@mod haskell-tools.strings
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Helper functions for working with strings
|
||||
---@brief ]]
|
||||
|
||||
---@class StringsUtil
|
||||
local Strings = {}
|
||||
|
||||
---Trim leading and trailing whitespace.
|
||||
---@param str string
|
||||
---@return string trimmed
|
||||
Strings.trim = function(str)
|
||||
return (str:match('^%s*(.*)') or str):gsub('%s*$', '')
|
||||
end
|
||||
|
||||
return Strings
|
||||
@ -0,0 +1,107 @@
|
||||
---@mod haskell-tools.tags haskell-tools fast-tags module
|
||||
|
||||
local HTConfig = require('haskell-tools.config.internal')
|
||||
local Types = require('haskell-tools.types.internal')
|
||||
local log = require('haskell-tools.log.internal')
|
||||
local compat = require('haskell-tools.compat')
|
||||
|
||||
local _state = {
|
||||
fast_tags_generating = false,
|
||||
projects = {},
|
||||
}
|
||||
|
||||
log.debug('Setting up fast-tags tools')
|
||||
local config = HTConfig.tools.tags
|
||||
|
||||
---@class GenerateProjectTagsOpts
|
||||
---@field refresh boolean Whether to refresh the tags if they have already been generated
|
||||
--- for the project (default: true)
|
||||
|
||||
---@class FastTagsTools
|
||||
local FastTagsTools = {}
|
||||
|
||||
---Generates tags for the current project
|
||||
---@param path string|nil File path
|
||||
---@param opts GenerateProjectTagsOpts|nil Options
|
||||
FastTagsTools.generate_project_tags = function(path, opts)
|
||||
path = path or vim.api.nvim_buf_get_name(0)
|
||||
opts = vim.tbl_extend('force', { refresh = true }, opts or {})
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local project_root = HtProjectHelpers.match_project_root(path)
|
||||
if not project_root then
|
||||
log.warn('generate_project_tags: No project root found.')
|
||||
return
|
||||
end
|
||||
if opts.refresh == false and _state.projects[project_root] then
|
||||
log.debug('Project tags already generated. Skipping.')
|
||||
return
|
||||
end
|
||||
_state.projects[project_root] = true
|
||||
_state.fast_tags_generating = true
|
||||
if project_root then
|
||||
log.debug('Generating project tags for' .. project_root)
|
||||
compat.system({ 'fast-tags', '-R', project_root }, nil, function(sc)
|
||||
if sc.code ~= 0 then
|
||||
log.error { 'Error running fast-tags on project root', sc.code, sc.stderr }
|
||||
end
|
||||
---@cast sc vim.SystemCompleted
|
||||
_state.fast_tags_generating = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Generate tags for the package containing `path`
|
||||
---@param path string|nil File path
|
||||
FastTagsTools.generate_package_tags = function(path)
|
||||
path = path or vim.api.nvim_buf_get_name(0)
|
||||
_state.fast_tags_generating = true
|
||||
local HtProjectHelpers = require('haskell-tools.project.helpers')
|
||||
local rel_package_root = HtProjectHelpers.match_package_root(path)
|
||||
if not rel_package_root then
|
||||
log.warn('generate_package_tags: No rel_package root found.')
|
||||
return
|
||||
end
|
||||
local package_root = vim.fn.getcwd() .. '/' .. rel_package_root
|
||||
local project_root = HtProjectHelpers.match_project_root(path) or vim.fn.getcwd()
|
||||
if not package_root then
|
||||
log.warn('generate_package_tags: No package root found.')
|
||||
return
|
||||
end
|
||||
if not project_root then
|
||||
log.warn('generate_package_tags: No project root found.')
|
||||
return
|
||||
end
|
||||
compat.system({ 'fast-tags', '-R', package_root, project_root }, nil, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
if sc.code ~= 0 then
|
||||
log.error { 'Error running fast-tags on package', sc.code, sc.stderr }
|
||||
end
|
||||
_state.fast_tags_generating = false
|
||||
end)
|
||||
end
|
||||
|
||||
if not Types.evaluate(config.enable) then
|
||||
return
|
||||
end
|
||||
|
||||
if vim.fn.executable('fast-tags') ~= 1 then
|
||||
local err_msg = 'haskell-tools: fast-tags fallback configured, but fast-tags executable not found'
|
||||
log.error(err_msg)
|
||||
vim.notify(err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local package_events = config.package_events
|
||||
if #package_events > 0 then
|
||||
vim.api.nvim_create_autocmd(package_events, {
|
||||
group = vim.api.nvim_create_augroup('haskell-tools-generate-package-tags', {}),
|
||||
pattern = { 'haskell', '*.hs' },
|
||||
callback = function(meta)
|
||||
if _state.fast_tags_generating then
|
||||
return
|
||||
end
|
||||
FastTagsTools.generate_package_tags(meta.file)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return FastTagsTools
|
||||
@ -0,0 +1,32 @@
|
||||
---@mod haskell-tools.internal-types
|
||||
|
||||
---@brief [[
|
||||
|
||||
---WARNING: This is not part of the public API.
|
||||
---Breaking changes to this module will not be reflected in the semantic versioning of this plugin.
|
||||
|
||||
--- Type definitions
|
||||
---@brief ]]
|
||||
|
||||
---@class HsEntryPoint
|
||||
---@field package_dir string
|
||||
---@field package_name string
|
||||
---@field exe_name string
|
||||
---@field main string
|
||||
---@field source_dir string
|
||||
|
||||
local Types = {}
|
||||
|
||||
---Evaluate a value that may be a function
|
||||
---or an evaluated value
|
||||
---@generic T
|
||||
---@param value (fun():T)|T
|
||||
---@return T
|
||||
Types.evaluate = function(value)
|
||||
if type(value) == 'function' then
|
||||
return value()
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
return Types
|
||||
Reference in New Issue
Block a user