Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(repl): ghci repl integration #37

Merged
merged 2 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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.
- Interact with the GHCi repl from any buffer using lua functions.
### Changed
- Do not close Hoogle Telescope prompt on `<C-b>` (open hackage docs in browser).
### Fixed
Expand Down
76 changes: 75 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,17 @@ 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)).
* Interact with the repl from any buffer using a lua API.

[![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 +170,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 +233,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
19 changes: 11 additions & 8 deletions lua/haskell-tools/project-util.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local deps = require('haskell-tools.deps')

-- Utility functions for analysing a project
-- Utility functions for analysing a project.
-- This module is not public API.
local M = {}

local function root_pattern(...)
Expand Down Expand Up @@ -31,22 +32,24 @@ 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

function M.get_package_name(path)
local package_path = M.match_package_root(path)
return package_path and vim.fn.fnamemodify(package_path, ':t')
end

return M
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