Skip to content

Commit

Permalink
feat: basic ghci integration
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcjkb committed Nov 8, 2022
1 parent c2a877a commit 25ed857
Show file tree
Hide file tree
Showing 9 changed files with 497 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- GHCi repl integration: Automagically detect the command to start GHCi and load the current buffer.
### Changed
- Do not close Hoogle Telescope prompt on `<C-b>` (open hackage docs in browser).
### Fixed
Expand Down
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ To get started quickly with the default setup, add the following to your neovim

```lua
local ht = require('haskell-tools')
local def_opts = { noremap = true, silent = true, }
ht.setup {
hls = {
-- See nvim-lspconfig's suggested configuration for keymaps, etc.
on_attach = function(client, bufnr)
local opts = { noremap = true, silent = true, buffer = bufnr }
local opts = vim.tbl_extend('keep', def_opts, { buffer = bufnr, })
-- haskell-language-server relies heavily on codeLenses,
-- so auto-refresh (see advanced configuration) is enabled by default
vim.keymap.set('n', '<space>ca', vim.lsp.codelens.run, opts)
Expand All @@ -71,6 +72,14 @@ ht.setup {
end,
},
}
-- Suggested keymaps that do not depend on haskell-language-server
-- Toggle a GHCi repl for the current package
vim.keymap.set('n', '<leader>rr', ht.repl.toggle, def_opts)
-- Toggle a GHCi repl for the current buffer
vim.keymap.set('n', '<leader>rf', funciton()
ht.repl.toggle(vim.api.nvim_buf_get_name(0))
end, def_opts)
vim.keymap.set('n', '<leader>rq', ht.repl.quit, def_opts)
```

If using a local `hoogle` installation, [follow these instructions](https://github.com/ndmitchell/hoogle/blob/master/docs/Install.md#generate-a-hoogle-database)
Expand Down Expand Up @@ -126,6 +135,16 @@ With the `<C-r>` keymap, the Hoogle search telescope integration can be used to

[![asciicast](https://asciinema.org/a/xEWKbTELrnJD0wNbC5t6jL6Tw.svg)](https://asciinema.org/a/xEWKbTELrnJD0wNbC5t6jL6Tw?t=0:04)

#### GHCi repl

Start a GHCi repl for the current project / buffer.

* Automagically detects the appropriate command (`cabal new-repl`, `stack ghci` or `ghci`) for your project.
* Choose between a builtin handler or [`toggleterm.nvim`](https://github.com/akinsho/toggleterm.nvim).
* Dynamically create a repl command for [`iron.nvim`](https://github.com/hkupty/iron.nvim) (see [advanced configuration](#advanced-configuration)).

[![asciicast](https://asciinema.org/a/HtTdq1tqxoRVjt4hEf22tInLV.svg)](https://asciinema.org/a/HtTdq1tqxoRVjt4hEf22tInLV)

### Planned

For planned features, refer to the [issues](https://github.com/MrcJkb/haskell-tools.nvim/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement).
Expand All @@ -150,6 +169,17 @@ require('haskell-tools').setup {
-- 'browser': Open hoogle search in the default browser.
mode = 'auto',
},
repl = {
-- 'builtin': Use the simple builtin repl
-- 'toggleterm': Use akinsho/toggleterm.nvim
handler = 'builtin',
builtin = {
create_repl_window = function(view)
-- create_repl_split | create_repl_vsplit | create_repl_tabnew | create_repl_cur_win
return view.create_repl_split { size = vim.o.lines / 3 }
end
},
},
},
hls = { -- LSP client options
-- ...
Expand Down Expand Up @@ -202,6 +232,49 @@ hls = {
},
```

### Set up [`iron.nvim`](https://github.com/hkupty/iron.nvim) to use `haskell-tools.nvim`

Depends on [iron.nvim/#300](https://github.com/hkupty/iron.nvim/pull/300).

```lua
local iron = require("iron.core")
iron.setup {
config = {
repl_definition = {
haskell = {
command = function(meta)
local file = vim.api.nvim_buf_get_name(meta.current_bufnr)
-- call `require` in case iron is set up before haskell-tools
return require('haskell-tools').repl.mk_repl_cmd(file)
end,
},
},
},
}
```

### Available functions

```lua
local ht = require('haskell-tools')

-- Run a hoogle signature for the value under the cursor
ht.hoogle.hoogle_signature()

-- Toggle a GHCi repl
ht.repl.toggle()
-- Toggle a GHCi repl for `file`
ht.repl.toggle(file)
-- Quit the repl
ht.repl.quit()
-- Paste a command to the repl from register `reg`. (`reg` defaults to '"')
ht.repl.paste(reg)
-- Query the repl for the type of register `reg`. (`reg` defaults to '"')
ht.repl.paste_type(reg)
-- Query the repl for the type of word under the cursor
ht.repl.cword_type()
```

## Troubleshooting

#### LSP features not working
Expand Down
11 changes: 11 additions & 0 deletions lua/haskell-tools/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ local defaults = {
-- -- TODO: Fall back to a hoogle search if goToDefinition fails
-- goToDefinitionFallback = false,
},
repl = {
-- 'builtin': Use the simple builtin repl
-- 'toggleterm': Use akinsho/toggleterm.nvim
handler = 'builtin',
builtin = {
create_repl_window = function(view)
-- create_repl_split | create_repl_vsplit | create_repl_tabnew | create_repl_cur_win
return view.create_repl_split { size = vim.o.lines / 3 }
end
},
},
},
hls = {
on_attach = function(...) end,
Expand Down
8 changes: 8 additions & 0 deletions lua/haskell-tools/deps.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ function M.require_lspconfig(modname)
return M.require_or_err(modname, 'neovim/nvim-lspconfig')
end

function M.require_toggleterm(modname)
return M.require_or_err(modname, 'akinsho/toggleterm')
end

function M.require_iron(modname)
return M.require_or_err(modname, 'hkupty/iron.nvim')
end

return M
4 changes: 4 additions & 0 deletions lua/haskell-tools/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local M = {
config = nil,
lsp = nil,
hoogle = nil,
repl = nil,
}

function M.setup(opts)
Expand All @@ -11,10 +12,13 @@ function M.setup(opts)
M.lsp = lsp
local hoogle = require('haskell-tools.hoogle')
M.hoogle = hoogle
local repl = require('haskell-tools.repl')
M.repl = repl

config.setup(opts)
lsp.setup()
hoogle.setup()
repl.setup()
end

return M
13 changes: 4 additions & 9 deletions lua/haskell-tools/project-util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,21 @@ function M.get_root_dir(path)
local lspconfig = deps.require_lspconfig('lspconfig')
return lspconfig.hls.get_root_dir(path)
end

-- Is `path` part of a cabal project?
-- @param string?: path to check for
-- @return boolean
-- @param string: path to check for
-- @return boolean | nil if `path` is not a writable file
function M.is_cabal_project(path)
path = path or vim.fn.expand('%')
local get_root = root_pattern('*.cabal', 'cabal.project')
return get_root(path) ~= nil
end

-- Is `path` part of a stack project?
-- @param string?: path to check for
-- @return boolean
-- @param string: path to check for
-- @return boolean | nil if `path` is not a writable file
function M.is_stack_project(path)
path = path or vim.fn.expand('%')
return M.match_stack_project_root(path) ~= nil
end

-- Get the package name for a path
-- @return string | nil
function M.get_package_name(path)
local package_path = M.match_package_root(path)
return package_path and vim.fn.fnamemodify(package_path, ':t')
Expand Down
125 changes: 125 additions & 0 deletions lua/haskell-tools/repl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
local ht = require('haskell-tools')
local project = require('haskell-tools.project-util')

-- Tools for interaction with a ghci repl
local M = {
}

-- Extend a repl command for `file`.
-- If `file` is `nil`, create a repl the nearest package.
-- @param cmd: table: the command to extend
-- @param file: string | nil
-- @param on_no_package: function(table) | nil: handler in case no package is found
-- @return table | nil
local function extend_repl_cmd(cmd, file, on_no_package)
on_no_package = on_no_package or function(_) return nil end
if file == nil then
file = vim.api.nvim_buf_get_name(0)
local pkg = project.get_package_name(file)
if pkg then
table.insert(cmd, pkg)
return cmd
else
return on_no_package(cmd)
end
end
local pkg = project.get_package_name(file)
if not pkg then return on_no_package(cmd) end
if vim.endswith(file, '.hs') then
table.insert(cmd, file)
else
table.insert(cmd, pkg)
end
return cmd
end

-- Create a cabal repl command for `file`.
-- If `file` is `nil`, create a repl the nearest package.
-- @param string | nil: file
-- @return table | nil
local function mk_cabal_repl_cmd(file)
return extend_repl_cmd({ 'cabal', 'new-repl', }, file)
end

-- Create a stack repl command for `file`.
-- If `file` is `nil`, create a repl the nearest package.
-- @param string | nil: file
-- @return table | nil
local function mk_stack_repl_cmd(file)
return extend_repl_cmd({ 'stack', 'ghci', }, file, function(cmd) return cmd end)
end

-- Create the command to create a repl for a file.
-- If `file` is `nil`, create a repl the nearest package.
-- @param string | nil: file
-- @return table | nil
function M.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
return nil
end
end
if project.is_cabal_project(chk_path) then
return mk_cabal_repl_cmd(file)
end
if project.is_stack_project(chk_path) then
return mk_stack_repl_cmd(file)
end
if vim.fn.executable('ghci') == 1 then
return vim.tbl_flatten { 'ghci', file and { file } or {}}
end
return nil
end

-- Create the command to create a repl for the current buffer.
-- @return table | nil
function M.buf_mk_repl_cmd()
local file = vim.api.nvim_buf_get_name(0)
return M.mk_repl_cmd(file)
end

function M.setup()
local opts = ht.config.options.tools.repl
local handler = {}
if opts.handler == 'toggleterm' then
local toggleterm = require('haskell-tools.repl.toggleterm')
toggleterm.setup(M.mk_repl_cmd)
handler = toggleterm
else
local builtin = require('haskell-tools.repl.builtin')
builtin.setup(M.mk_repl_cmd, opts.builtin)
handler = builtin
end
-- Toggle a GHCi repl
-- @param string?: optional file path
M.toggle = handler.toggle

-- Quit the repl
M.quit = handler.quit

-- Paste from register `reg` to the repl
-- @param string?: register (defaults to '"')
function M.paste(reg)
local data = vim.fn.getreg(reg or '"')
handler.send_cmd(data)
end

-- Query the repl for the type of register `reg`
-- @param string?: register (defaults to '"')
function M.paste_type(reg)
local data = vim.fn.getreg(reg or '"')
handler.send_cmd(':t ' .. data)
end

-- Query the repl for the type of word under the cursor
-- @param string?: register (defaults to '"')
function M.cword_type()
local cword = vim.fn.expand('<cword>')
handler.send_cmd(':t ' .. cword)
end

end

return M
Loading

0 comments on commit 25ed857

Please sign in to comment.