Language Server Protocol implementation for .iop files, providing:
- Go-to-definition — jump from a type reference to its definition
- Hover documentation — show doc comments and type info on hover
- C file support — go-to-definition and hover for IOP-generated C
identifiers (e.g.,
tstiop__my_struct_a__t→tstiop.MyStructAin the.iopsource)
- Python ≥ 3.9
- uv (recommended) or pip
- tree-sitter-iop checked out locally
cd /path/to/iop-lsp
uv sync # Creates .venv and installs all dependenciesThe pyproject.toml references tree-sitter-iop as a local path dependency
(../tree-sitter-iop). If your checkout is elsewhere, edit
[tool.uv.sources] in pyproject.toml to point to it, then run
uv sync again.
# Start the LSP server (editors do this automatically)
uv run --project /path/to/iop-lsp python -m iop_lsp --stdio
# With logging (useful for debugging)
uv run --project /path/to/iop-lsp python -m iop_lsp --stdio \
--log-file /tmp/iop-lsp.log -vThe LSP server uses standard LSP over stdio and works with any editor.
On startup, it recursively indexes all .iop files under the workspace
root to build a symbol table used for go-to-definition and hover.
No plugins required — Neovim ≥ 0.11 has built-in LSP support.
Quick install: run ./install-nvim-iop-lsp.sh to automatically
configure your ~/.vimrc (use --uninstall to revert).
Manual setup: add the following to your Neovim configuration
(e.g., ~/.config/nvim/init.lua):
-- Teach Neovim about the .iop file extension.
vim.filetype.add({
extension = { iop = 'iop' },
})
-- Start iop-lsp for .iop.
-- Adjust the --project path to where you cloned iop-lsp.
vim.api.nvim_create_autocmd('FileType', {
pattern = { 'iop' },
callback = function()
vim.lsp.start({
name = 'iop-lsp',
cmd = {
'uv', 'run',
'--project', '/path/to/iop-lsp',
'python', '-m', 'iop_lsp', '--stdio',
},
-- Walk up to .git so the LSP indexes all .iop files
-- in the project. Cross-file go-to-definition only
-- works within this root.
root_dir = vim.fs.root(0, { '.git' }),
})
end,
})How it works:
root_dirdetermines which.iopfiles are indexed. The LSP recursively scans this directory on startup. Set it to the root of your project (e.g., lib-common) so that all type definitions are available for cross-file navigation.vim.lsp.start()reuses the same LSP server for all buffers that share the sameroot_dir, so opening multiple.iopfiles in the same project is efficient.
Keybindings: Neovim ≥ 0.11 maps K to hover automatically.
Go-to-definition must be mapped manually (e.g., via an LspAttach
autocmd or your editor distribution's LSP keybindings). The built-in
gd is Vim's "go to local declaration" and does not call the LSP.
Example keybindings:
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
local opts = { buffer = args.buf }
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
end,
})If you have
tree-sitter-iop
built locally with its iop.so parser, you can enable syntax
highlighting for .iop files without any plugin:
-- Adjust the path to your tree-sitter-iop checkout.
local ts_iop = '/path/to/tree-sitter-iop'
pcall(function()
vim.treesitter.language.add('iop', { path = ts_iop .. '/iop.so' })
vim.opt.runtimepath:prepend(ts_iop) -- finds queries/iop/highlights.scm
end)
vim.api.nvim_create_autocmd('FileType', {
pattern = 'iop',
callback = function() vim.treesitter.start() end,
})When you use IOP-generated types in C code (e.g., tstiop__my_struct_a__t),
iop-lsp can resolve them back to their .iop source definitions. This works
for .c, .h, and .blk files.
To enable this, configure your editor to attach iop-lsp to C files before
clangd. iop-lsp only responds for identifiers it recognizes (IOP C names)
and returns null for everything else, so the editor falls back to clangd
for normal C symbols.
Add c and h to the FileType pattern so iop-lsp attaches to C files too:
vim.api.nvim_create_autocmd('FileType', {
pattern = { 'iop', 'c' },
callback = function()
vim.lsp.start({
name = 'iop-lsp',
cmd = {
'uv', 'run',
'--project', '/path/to/iop-lsp',
'python', '-m', 'iop_lsp', '--stdio',
},
root_dir = vim.fs.root(0, { '.git' }),
})
end,
})When both iop-lsp and clangd are attached, Neovim sends requests to all
servers. To ensure iop-lsp takes priority for go-to-definition on IOP types,
list it first in vim.lsp.enable() or use a custom gd keymap that prefers
iop-lsp.
Emacs ≥ 29 ships with Eglot,
a built-in LSP client. The easiest way to run both clangd and
iop-lsp on the same buffer is to use
rassumfrassum, an LSP
multiplexer.
Create a rassumfrassum preset (e.g. ~/.config/rass/presets/cmode-iop.py):
def servers():
return [
['clangd'],
['uv', 'run', '--project', '/path/to/iop-lsp', '-m', 'iop_lsp',
'--stdio']
]Then register the preset with Eglot:
(add-to-list 'eglot-server-programs
'((c++-mode c-mode) . ("rass" "cmode-iop")))Add to ~/.config/helix/languages.toml:
[language-server.iop-lsp]
command = "uv"
args = ["run", "--project", "/path/to/iop-lsp",
"python", "-m", "iop_lsp", "--stdio"]
[[language]]
name = "iop"
scope = "source.iop"
file-types = ["iop"]
comment-token = "//"
block-comment-tokens = { start = "/*", end = "*/" }
indent = { tab-width = 4, unit = " " }
language-servers = ["iop-lsp"]Then in Helix:
gd— Go to definition (on a type reference)<space>k— Show hover documentation
List iop-lsp before clangd in the language-servers array for C:
[[language]]
name = "c"
language-servers = ["iop-lsp", "clangd"]The first server in the array gets priority per-feature, so iop-lsp results are preferred when available.
uv run python -m pytest tests/ -viop_lsp/server.py— LSP server, request handlersiop_lsp/indexer.py— Parse .iop files with tree-sitter, build symbol tableiop_lsp/symbols.py— Symbol data structuresiop_lsp/c_mapping.py— IOP ↔ C name conversions (CamelCase ↔ snake_case)iop_lsp/doc_comments.py— Extract doc comments from ASTiop_lsp/schema.py— IOP type resolution for YAML supportiop_lsp/yaml_support.py— YAML parsing and cursor-to-type mapping