Skip to content

Language Server

The compiler ships a language server. CKSP Tools for Visual Studio Code is built on it, but nothing about it is specific to that editor: it speaks plain LSP over stdio, so any client that can launch a process and talk JSON-RPC can integrate against it.

This chapter is the contract for that integration. If you are looking for what the VS Code extension does rather than how to build another one, see Language and Editor.


Launching

cksp --lsp

The server reads requests from stdin and writes responses to stdout, framed with Content-Length headers as the LSP specification requires. It writes nothing else to stdout, so the stream stays clean for the protocol.

Do not pass an input file

--lsp replaces the compile run. Passing a source file alongside it is not an LSP session.

The server answers every request it receives. A method it does not implement is answered with JSON-RPC error -32601 rather than ignored, so a client waiting on a response never hangs.


Source files

The server recognizes these extensions and ignores everything else:

Extension Contents
.cksp CKSP source
.ksp vanilla KSP source
.txt vanilla KSP source, as Kontakt exports it
.nckp Kontakt performance view

A client should register its document selector and file watchers for exactly this set.


Capabilities

The initialize response advertises the following. Reading it at runtime is the reliable source — this table is here so you know what to expect before you build against it.

Capability Value Notes
textDocumentSync openClose: true, change: 1 Full sync. Incremental changes are not supported; send the whole document.
definitionProvider true Returns LocationLink[], not Location[].
referencesProvider true Honours context.includeDeclaration.
renameProvider prepareProvider: true Call prepareRename first; it refuses positions that cannot be renamed.
documentHighlightProvider true
documentLinkProvider resolveProvider: false Links on import paths and on #pragma file arguments.
completionProvider triggerCharacters: ["."], resolveProvider: false Items carry labelDetails; see below.
codeActionProvider codeActionKinds: ["quickfix"] See Quick fixes.

serverInfo reports the name cksp-lsp and the compiler version, which is the same version string cksp --version prints.

Methods

Requests: initialize, shutdown, textDocument/definition, textDocument/references, textDocument/prepareRename, textDocument/rename, textDocument/documentHighlight, textDocument/completion, textDocument/codeAction, textDocument/documentLink.

Notifications: initialized, textDocument/didOpen, textDocument/didChange, textDocument/didClose, workspace/didChangeWatchedFiles, exit.

resolveProvider is false everywhere

Completion items and document links arrive complete. There is no completionItem/resolve or documentLink/resolve step to implement.

Completion item details

Items set labelDetails, which a client only renders when it announced completionItem.labelDetailsSupport in its own client capabilities. Without it the parameter list and the construct word (function, struct, namespace, …) are dropped and every item looks the same. Announce it.


Entry points

This is the part a client cannot infer from the protocol, and getting it wrong is the difference between a language server that understands a project and one that only understands single files.

CKSP analyses entry points, not open files. An entry point is a file that is compiled on its own — typically the script that carries on init and pulls the rest in through import. Everything reachable from it is analysed as part of it, which is what makes go-to-definition and completion work across file boundaries.

Tell the server about them in initializationOptions:

initialize params
{
  "rootUri": "file:///path/to/project",
  "initializationOptions": {
    "mainFilePath": "src/main.cksp",
    "entryPoints": [
      "src/main.cksp",
      "src/second_script.cksp"
    ]
  }
}
  • entryPoints — an array of paths or file:// URIs. Relative paths are resolved against the workspace root.
  • mainFilePath / mainFileUri — a single entry point, accepted as a convenience. It is added to the list, so you may use either or both.

Configured entry points are analysed as soon as initialize returns, before any file is opened. That means project-wide diagnostics are available immediately, and a file that is only ever imported is understood in the context of the script that imports it.

If you configure nothing

A file that is opened without belonging to any configured entry point is treated as an entry point of its own. That works, but a file meant to be imported will then be analysed without the declarations its importer provides, and will report errors that the real project does not have. Configure entry points if your users have projects.


Diagnostics

Diagnostics are published with textDocument/publishDiagnostics after an analysis run.

Analysis is debounced by 120 ms after the last change. The server publishes for the entry source even when there are no diagnostics — an empty array is a real message, not silence. That makes publishDiagnostics a usable barrier: if you need to know that an edit has been analysed, wait for the next publication rather than sleeping.

The code field carries the compiler's error category (ParseError, TypeError, VariableError, SyntaxError, PreprocessorError, …). The message field contains the compiler's explanation together with its Expected: / Got: detail, joined into one string.


Quick fixes

The server offers quick fixes for a growing set of diagnostics — converting a SublimeKSP taskfunc into a function, correcting a name that differs only in case, replacing a SublimeKSP {#pragma …}, and others.

Send the diagnostic's data field back

A diagnostic that has a fix carries it in its data field. textDocument/codeAction builds the action from the diagnostics in context.diagnostics, so a client that drops data when it stores or re-sends diagnostics gets no quick fixes at all, with no error to explain it.

The LSP specification says clients should preserve data. Not all do. If quick fixes are silently missing in your integration, this is why.

The shape, should you want to inspect it:

diagnostic.data
{
  "fixKind": "ConvertTaskfuncToFunction",
  "title": "Convert taskfunc 'get_random_value' to a function",
  "isPreferred": true,
  "edits": [
    {
      "targetUri": "file:///path/to/script.cksp",
      "range": { "start": {"line": 4, "character": 0},
                 "end":   {"line": 4, "character": 8} },
      "newText": "function"
    }
  ]
}

You do not need to read it. Send the diagnostics back unchanged in context.diagnostics and the server returns finished CodeActions with a WorkspaceEdit attached. Passing context.only: ["quickfix"] is honoured; omitting only returns everything.

A fix may edit more than one range and more than one file, so apply the returned WorkspaceEdit as a whole rather than assuming a single edit.

Fixes can cascade

Applying one fix may reveal the next — converting a taskfunc leaves a deprecated return syntax that has a fix of its own. A client that re-requests code actions after applying an edit lets a user port a file by clicking repeatedly.


File watching

The server handles workspace/didChangeWatchedFiles and uses it to keep the import graph honest when files change outside the editor. Register watchers for the source extensions; without them, a file edited or deleted outside the editor leaves stale analysis behind.

Unopened files are read from disk; open documents are analysed from the buffer the client sent, not from what is saved.


Shutdown

Standard LSP: the shutdown request, answered with null, followed by the exit notification, on which the process terminates.