Language Server Protocol (LSP) Guide

Turmeric ships a built-in language server that speaks Language Server Protocol over stdio (JSON-RPC 2.0). The server is invoked as:

tur lsp

Editors launch it as a subprocess and communicate via stdin/stdout.

Current capabilities

Capability Status
Diagnostics (textDocument/publishDiagnostics) Supported
Document symbols (textDocument/documentSymbol) Supported
Workspace symbols (workspace/symbol) Supported (open documents only)
Hover documentation Supported
Go-to-definition Supported
Completion (textDocument/completion) Supported
Signature help (textDocument/signatureHelp) Supported
Formatting (textDocument/formatting) Supported
Document highlight (textDocument/documentHighlight) Supported, scope-aware
Rename (textDocument/prepareRename, textDocument/rename) Supported
References (textDocument/references) Supported
Cancellation ($/cancelRequest) Supported
Semantic tokens Not supported

When you open or edit a .tur file, the server compiles it in check-only mode and publishes any parse or type errors back to the editor as diagnostics (red underlines, error panel entries, etc.). The document's extension selects its reader the way it does for the compiler -- a .tur.sweet file is sweet-exp and a .scm file is Scheme with no #lang line needed -- and a #lang line takes over when the extension says plain Turmeric.

Behaviours worth knowing about:

Scope, highlight, rename and references

The symbol index records global bindings. On its own that is enough for hover and an outline and not enough for any question about a name the cursor is standing inside: a parameter named x has no entry at all, so highlighting it matched every x in the file.

A second pass (src/lsp/lsp_scope.c) records the locals -- defn and fn parameters, let and letrec bindings, match binders -- each with the source region it is visible in. It rides the same elaboration hook the symbol harvest does, so it is one walk over the tree the compiler already built, not a second front end.

What that buys:

Editor configuration

Neovim (nvim-lspconfig)

Add a custom server entry -- nvim-lspconfig does not bundle Turmeric by default, so you register it manually:

local lspconfig = require("lspconfig")
local configs   = require("lspconfig.configs")

if not configs.turmeric then
  configs.turmeric = {
    default_config = {
      cmd        = { "tur", "lsp" },
      filetypes  = { "tur" },
      root_dir   = lspconfig.util.root_pattern("build.tur", ".git"),
      single_file_support = true,
    },
  }
end

lspconfig.turmeric.setup({})

If you use lazy.nvim and want this to load alongside nvim-lspconfig:

{
  "neovim/nvim-lspconfig",
  config = function()
    -- paste the block above here
  end,
}

Neovim does not auto-detect .tur files as tur filetype. Add this to your config or to ~/.config/nvim/ftdetect/tur.lua:

vim.filetype.add({ extension = { tur = "tur" } })

Neovim (built-in LSP, no plugin)

vim.api.nvim_create_autocmd("FileType", {
  pattern = "tur",
  callback = function(ev)
    vim.lsp.start({
      name    = "turmeric",
      cmd     = { "tur", "lsp" },
      root_dir = vim.fs.dirname(
        vim.fs.find({ "build.tur", ".git" }, { upward = true })[1]
      ),
    })
  end,
})

Vim (vim-lsp)

Install vim-lsp, then add:

if executable('tur')
  au User lsp_setup call lsp#register_server({
    \ 'name': 'turmeric',
    \ 'cmd': {server_info -> ['tur', 'lsp']},
    \ 'allowlist': ['tur'],
    \ })
endif

Add filetype detection if not already present:

augroup TurmericFt
  autocmd!
  autocmd BufRead,BufNewFile *.tur setfiletype tur
augroup END

Vim (ALE)

ALE can drive an LSP binary directly. Add to your ~/.vim/after/ftplugin/tur.vim (or equivalent):

let g:ale_linters = { 'tur': ['turmeric_lsp'] }

Then register the linter in your vimrc or in ~/.vim/ale_linters/tur/turmeric_lsp.vim:

call ale#linter#Define('tur', {
\   'name':            'turmeric_lsp',
\   'lsp':             'stdio',
\   'executable':      'tur',
\   'command':         '%e lsp',
\   'project_root':    function('ale#util#FindProjectRoot'),
\   'language':        'tur',
\ })

VS Code

The VS Code extension provides syntax highlighting, formatting and an LSP client that spawns tur lsp (see below).

A minimal settings.json entry using the multi-lsp extension:

{
  "multi-lsp.servers": [
    {
      "language": "tur",
      "command": "tur",
      "args": ["lsp"]
    }
  ]
}

The vscode-turmeric extension already speaks LSP natively via vscode-languageclient, spawning tur lsp over stdio; the turmeric.serverPath setting overrides which tur it uses. The multi-lsp recipe above is only needed if you would rather drive the server yourself.

Emacs (eglot)

Eglot is built into Emacs 29+. Add a major mode for Turmeric first (or use lisp-mode as a fallback), then register the server:

;; Simple major mode derived from lisp-mode
(define-derived-mode turmeric-mode lisp-mode "Turmeric"
  "Major mode for Turmeric source files.")
(add-to-list 'auto-mode-alist '("\\.tur\\'" . turmeric-mode))

;; Register the LSP server with eglot
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(turmeric-mode . ("tur" "lsp"))))

;; Auto-start eglot when opening .tur files
(add-hook 'turmeric-mode-hook #'eglot-ensure)

Emacs (lsp-mode)

(with-eval-after-load 'lsp-mode
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection '("tur" "lsp"))
    :activation-fn  (lsp-activate-on "tur")
    :server-id      'turmeric)))

(add-hook 'turmeric-mode-hook #'lsp)

Helix

Add to ~/.config/helix/languages.toml:

[[language]]
name              = "turmeric"
scope             = "source.tur"
file-types        = ["tur"]
comment-token     = ";"
indent            = { tab-width = 2, unit = "  " }
language-servers  = ["turmeric-lsp"]

[language-server.turmeric-lsp]
command = "tur"
args    = ["lsp"]

Zed

In ~/.config/zed/settings.json:

{
  "lsp": {
    "turmeric": {
      "binary": {
        "path": "tur",
        "arguments": ["lsp"]
      }
    }
  }
}

Zed also requires a language extension for filetype detection. Until an official extension is published, use the Zed extension API to register .tur manually or rely on generic highlighting.

How it works

When a .tur file is opened or modified, the server:

  1. Writes the current buffer text to a temporary file in the platform temp directory ($TMPDIR on Unix, %TMP%/%TEMP% on Windows).
  2. Runs the Turmeric compiler in type-check-only mode (tur_check_only).
  3. Collects all diagnostics via the internal diag_lsp_* API.
  4. Remaps temp-file paths back to the real document URI.
  5. Adopts the collected symbols as the document's index — unless the compile produced none and failed, in which case the previous index is kept (see "the last good symbol index is retained" above).
  6. Sends a textDocument/publishDiagnostics notification to the editor.
  7. Deletes the temporary file.

Diagnostics whose span is zero-width are widened by one character before they go out. A zero-width range paints nothing, so the diagnostic would be present in the response but invisible in the editor.

The server uses TextDocumentSyncKind.Full (sync kind 1): the entire file content is sent on every change, not just diffs. This keeps the implementation simple at the cost of slightly more data per keystroke for large files.

Troubleshooting

No diagnostics appear. Verify tur is on your $PATH:

which tur
tur --version

Server exits immediately. Run tur lsp in a terminal and type a minimal JSON-RPC initialize request. Any startup error (missing stdlib, bad install) will appear on stderr.

Diagnostics are stale or missing after save. Some editors only sync on save (not on every keystroke). Check your editor's LSP sync setting; the server responds to both textDocument/didOpen and textDocument/didChange.

See also