🚧 WIP: you're very welcome to try this, but I'm breaking a lot at the moment (October 16th)
Hackable LSP — @cathalogrady
This project has 2 goals.
- A generic LSP server that parses CLI tools, or indeed any program that outputs to STDOUT, such as linters, formatters, style checkers, etc and converts their output to LSP-compatible behaviour.
- An easily-forkable template to start your own custom LSP server using Pygls.
Because the heavy-lifting of this language server is done by external tooling (think pylint
, jq
, markdownlint
, etc), there is minimal implementation-specific code in this repo. That is to say that the majority of the code here is applicable to any language server built with Pygls. Or at the very least, it demonstrates a reasonable starting point. Deleting the super_glass_lsp/lsp/custom
folder should leave the codebase as close as possible to the minimum starting point for your own custom language server. Then you will also want to rename occurrences of [C|c]ustom
to your own language server's name.
pip install super-glass-lsp
Once you've installed the language server and set it up in your editor, it should be as easy as this to add new features (this is YAML, but your editor likely has its own config format):
# This is jsut an ID, so can be anything. Internally it's important so that you can
# override existing configs (either the bundled defaults, or configs you have
# created elsewhere): all configs with the same ID are automaticallly merged.
fuzzy_similar_words_completion:
# This is the part of the language server to which the `command` will apply.
# The other currently supported features are: `diagnostic`.
lsp_feature: completion
# This is the external command which will be triggered and parsed for every
# invocation of the feature. In the case of completions, editors will generally
# trigger it for _every_ character change, or even every key press. So be
# careful not to make this too expensive.
#
# Default behaviour is to pipe the entire contents of the file into the command.
# This can be overriden with `piped: false`. In which case you will likely want
# to manually do something with the file. You can access its path with the `{file}`
# token. Eg; `command: "cat {file} | tr ..."`.
#
# This particular command first breaks up the file into a list of words, which are
# then piped into a fuzzy finder, which then queries the list with the particular
# word currently under your cursor in the editor. Finally the results of the fuzzy
# search are deduplicated (with `uniq`).
#
# The command is run in a shell, so all the tools from your own machine are available.
command: "tr -cs '[:alnum:]' '\n' | fzf --filter='{word}' | uniq"
The server comes with a lot of defaults. To enable a particular tool simple provide the enabled: true
field for that tool. For example:
# This is YAML, but should be whatever format your editor's config is
initialization_options:
configs:
jqlint:
enabled: true
TODO:
-
Explain all the fields and tokens for each LSP feature
-
Remember to describe the format array lines priorities
-
How to set up the debug logs. But also maybe a LSP option to get all the debug in your editor
- Mention the test logs too. Because the E2E tests run in a subprocess
-
Remember to advise that some diagnostic tools output on STDERR, not STDOUT
-
Can you have both a Super Glass app and default configs?? I think at the moment you can't? (I don't think so)
Because this is a generic language server, the filetype/language that the server applies to varies depending on the config you've setup. It would be a bad idea for a generic language server to tell an editor that it wants to connect with every possible filetype/language (although this can be enabled on a per tool basis with the language_ids: ["*"]
setting). Instead, it is better that you manually inform your editor which filetypes/languages this generic server should be enabled for. How that is done is unique to each editor's config, I've tried to include examples for each one.
Neovim Lua (vanilla Neovim without `lspconfig`)
Since this project is very beta, we're not yet submitting this language server to the LSP Config plugin (the defacto way to add new language servers). Therefore, for now, we have to use Neovim's vanilla LSP setup (which has actually simplified a lot recently).
vim.api.nvim_create_autocmd({ "BufEnter" }, {
-- NB: You must remember to manually put the file extension pattern matchers for each LSP filetype
pattern = { "*" },
callback = function()
vim.lsp.start({
name = "super-glass",
cmd = { "super-glass-lsp" },
root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]),
init_options = {
configs = {
fuzzy_buffer_tokens = {
lsp_feature = "completion",
command = "tr -cs '[:alnum:]' '\n' | fzf --filter='{word}' | uniq",
},
}
},
})
end,
})
Vim (`vim-lsp`)
augroup LspSuperGlass
au!
autocmd User lsp_setup call lsp#register_server({
\ 'name': 'super-glass',
\ 'cmd': {server_info->['super-glass-lsp', '--logfile', 'path/to-logfile']},
\ 'allowlist': ['vim', 'eruby', 'markdown', 'yaml'],
\ 'initialization_options': { "configs":
\ { "fuzzy_buffer_tokens": {
\ "lsp_feature": "completion",
\ "command": "tr -cs '[:alnum:]' '\n' | fzf --filter='{word}' | uniq",
\ }
\ }
\ }})
augroup END
Neovim (`lspconfig`) TBC
Once we're stable, we'll submit ourselves for inclusion.
Emacs (`lsp-mode`)
(make-lsp-client :new-connection
(lsp-stdio-connection
`(,(executable-find "super-glass-lsp") "--logfile" "path/to/logs"))
:activation-fn (lsp-activate-on "json")
:initialization-options ; TODO: I'm not an Emacs user, how do we provide these options?
:server-id 'super-glass-lsp')))
Emacs (`eglot`) TBC
Once we're stable, we'll submit ourselves for inclusion.
VSCode TBC
Can we copy EFM's VSCode extension? https://github.com/Matts966/efm-langserver-vscode
Uses @alcarney's pytest-lsp module for end-to-end testing.
poetry run python -m pytest
This projects takes a lot of inspiration from @alcarney's fantastic Sphinx/RST LSP server Esbonio.
Logo is from a sticker I found on Amazon, obviously want a proper logo before I publish.