Isabelle LSP configuration for nvim-lspconfig.
Neovim does not have an isabelle filetype, so you have to add one yourself or install a plugin which introduces one (like isabelle-syn.nvim). See Quickstart on how to do so.
Mind you also that the language server needs a little bit before it starts up. When you open a .thy
file, it will start the language server in the background and open an output panel once it's running.
This plugin currently requires changeset aa77be3e8329 or later of Isabelle to work.
Install with your package manager of choice, e.g. lazy.nvim:
require('lazy').setup({
{
'Treeniks/isabelle-lsp.nvim',
branch = 'isabelle-language-server',
dependencies = {
'neovim/nvim-lspconfig'
},
},
})
In the context of my bachelor's thesis, I have contributed several changes to the Isabelle language server. These have been upstreamed as of 2024-10-02, however they are not part of any Isabelle release yet. You will need changeset aa77be3e8329 or later. If you also want these changes to work within Isabelle/VSCode, you'll need at least changeset e0327a38bf4d. You can clone the Isabelle repository with Mercurial from https://isabelle-dev.sketis.net/source/isabelle/. There is also a GitHub mirror if you prefer to use git.
hg clone https://isabelle-dev.sketis.net/source/isabelle/
cd isabelle
echo "isabelle-language-server" >> ./etc/ISABELLE_IDENTIFIER
Initialize Isabelle:
./Admin/init
# will happen automatically if you open a theory file
# but since it takes a long time it's more convenient to do it now
./bin/isabelle build -b HOL
vim.filetype.add({
extension = {
thy = 'isabelle',
},
})
Alternatively you can install a plugin that creates this filetype for you, like isabelle-syn.nvim (which will also include some static syntax highlighting).
require('isabelle-lsp').setup({
isabelle_path = '/path/to/isabelle/bin/isabelle',
})
The isabelle_path
line if optional if the isabelle
binary is already in your PATH.
local lspconfig = require('lspconfig')
lspconfig.isabelle.setup({})
Refer to nvim-lspconfig
's instructions on how to set up the language server client and keybinds.
If you want to open a vertical split instead of a horizontal split when you open an Isabelle file, you can specify so in the setup:
require('isabelle-lsp').setup({
vsplit = true,
})
Isabelle has its own concept of Symbols. For simplicity, symbols can be either shown in a unicode representation (⟹
) or in their ascii representation (\<Longrightarrow>
). By default, the language server will always use the ascii representation. If you use isabelle-syn.nvim, this isn't a big problem because that plugin uses vim's conceal feature to display the correct symbols anyway, however this might still be annoying to edit in practice.
Thus, there are two options to adjust the behaviour: unicode_symbols_output
and unicode_symbols_edits
.
unicode_symbols_output
: adjusts what representation should be used in output and state panels, as well as other displayed messagesunicode_symbols_edits
: adjusts what representation should be used for anything editing the buffer, e.g. completions and code actionsBoth settings are false by default.
require('isabelle-lsp').setup({
unicode_symbols_output = true,
unicode_symbols_edits = true,
})
Additionally, if you want to convert the current buffer from ascii to unicode representations, you can use the :SymbolsConvert
command.
You will need to use a font in your terminal that supports the special symbols of Isabelle. JuliaMono has worked quite well for me.
Isabelle has dynamic syntax highlighting. While isabelle-syn.nvim offers some static syntax highlighting, most of it will be overwritten by the language server. In order to customize the scopes used for each Isabelle decoration type, you can overwrite the hl_group_map
table:
require('isabelle-lsp').setup({
-- setting false means "don't do any highlighting for this group"
-- these are the defaults currently in use
hl_group_map = {
['background_unprocessed1'] = false,
['background_running1'] = false,
['background_canceled'] = false,
['background_bad'] = false,
['background_intensify'] = false,
['background_markdown_bullet1'] = 'markdownH1',
['background_markdown_bullet2'] = 'markdownH2',
['background_markdown_bullet3'] = 'markdownH3',
['background_markdown_bullet4'] = 'markdownH4',
['foreground_quoted'] = false,
['text_main'] = 'Normal',
['text_quasi_keyword'] = 'Keyword',
['text_free'] = 'Function',
['text_bound'] = 'Identifier',
['text_inner_numeral'] = false,
['text_inner_quoted'] = 'String',
['text_comment1'] = 'Comment',
['text_comment2'] = false, -- seems to not exist in the LSP
['text_comment3'] = false,
['text_dynamic'] = false,
['text_class_parameter'] = false,
['text_antiquote'] = 'Comment',
['text_raw_text'] = 'Comment',
['text_plain_text'] = 'String',
['text_overview_unprocessed'] = false,
['text_overview_running'] = 'Bold',
['text_overview_error'] = false,
['text_overview_warning'] = false,
['dotted_writeln'] = false,
['dotted_warning'] = "DiagnosticWarn",
['dotted_information'] = false,
['spell_checker'] = 'Underlined',
['text_inner_cartouche'] = false,
['text_var'] = 'Function',
['text_skolem'] = 'Identifier',
['text_tvar'] = 'Type',
['text_tfree'] = 'Type',
['text_operator'] = 'Function',
['text_improper'] = 'Keyword',
['text_keyword3'] = 'Keyword',
['text_keyword2'] = 'Keyword',
['text_keyword1'] = 'Keyword',
['foreground_antiquoted'] = false,
}
})
To enable server-side logging:
require('isabelle-lsp').setup({
log = '~/isabelle.log',
verbose = true, -- false by default
})
The following was tested before my language server changes existed and has not been tested since. I have no idea if it currently works.
This plugin can work on Windows, but it requires a little more setup and can be rather jank. It's also pretty slow (it takes ~20 seconds to start the server).
If you use WSL, it's probably better to use Isabelle and Neovim under WSL as well. I would not recommend mixing the two, although it seems possible.
You'll need to either have a sh
binary on PATH, or define a sh_path
in your config:
-- for MSYS2's sh
require('isabelle-lsp').setup({
sh_path = 'C:\\msys64\\usr\\bin\\sh.exe'
})
-- for WSL's bash
require('isabelle-lsp').setup({
sh_path = 'C:\\Windows\\system32\\bash.exe'
-- usually just 'bash' is enough as it's typically on PATH
})
-- for Cygwin, example installed with scoop
require('isabelle-lsp').setup({
sh_path = 'C:\\Users\\USERNAME\\scoop\\apps\\cygwin\\current\\root\\bin\\sh.exe'
})
Note this only has to be sh-like. I.e. you can also use bash
or fish
if you like, as long as they support a -c
CLI argument.
Note also that WSL forces a bash
program on PATH, so if you want to use MSYS2's bash and have WSL installed, you will have to specify the full path.
You will need to edit the isabelle-path
setting to your shell's path translation:
Instead of using C:\\isabelle\\...
use
C:/isabelle/...
or /c/isabelle/...
/mnt/c/isabelle/...
C:/isabelle/...
or /cygdrive/c/isabelle/...
Bad theory import
error when opening a file. The only way I found to fix this for now is to just hover the import and do an LSP goto definition or open the imported file manually. After going back, the error will probably™ go away.