lsp-plugin
stableLSP (Language Server Protocol) message building and parsing helpers for constructing JSON-RPC requests, responses, notifications, diagnostics, completions, and hover results.
use plugin lsp Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.
Functions (19)
- create_request Build a JSON-RPC request string
- create_response Build a JSON-RPC response string
- create_notification Build a JSON-RPC notification string
- create_error_response Build a JSON-RPC error response string
- encode_lsp_message Wrap JSON body with Content-Length header
- decode_lsp_header Parse Content-Length from a raw header
- position Create an LSP Position object
- range Create an LSP Range object
- diagnostic Create an LSP Diagnostic object
- completion_item Create an LSP CompletionItem object
- hover Create an LSP Hover result object
- document_symbol Create an LSP DocumentSymbol object
- text_edit Create an LSP TextEdit object
- parse_message Parse a JSON string into a table
- severity_error Returns diagnostic severity 1 (Error)
- severity_warning Returns diagnostic severity 2 (Warning)
- severity_info Returns diagnostic severity 3 (Information)
- severity_hint Returns diagnostic severity 4 (Hint)
- publish_diagnostics Build a publishDiagnostics notification
From import to result
- 01Import only what you use
- 02Copy the smallest example
- 03Run it with the Zolo CLI
Overview¶
lsp-plugin is a toolkit for speaking the Language Server Protocol from Zolo. It
has no connection or server state of its own: every function is a pure builder or
parser that turns Zolo values into the JSON-RPC and LSP shapes a client expects,
or turns a raw wire message back into a Zolo table. Requests, responses,
notifications, and error responses come out as ready-to-send JSON strings, while
the structural helpers (position, range, diagnostic, completion_item,
hover, document_symbol, text_edit) produce the nested tables those messages
carry as their params/result payloads.
The mental model is layered: build a payload table with the structural helpers,
wrap it in a JSON-RPC envelope with create_request / create_response /
create_notification, then frame it for the wire with encode_lsp_message. On
the way in you reverse the flow — decode_lsp_header reads the Content-Length
and parse_message decodes the JSON body into a table. The severity_*
constants and publish_diagnostics shortcut cover the most common diagnostics
path. Reach for this plugin when implementing a language server, an LSP client,
or any tooling that exchanges JSON-RPC messages.
Common patterns¶
Build a hover response payload and frame it for the wire in one pass:
use plugin lsp::{hover, range, create_response, encode_lsp_message}
let r = range(10, 4, 10, 9)
let h = hover("**let** — declares an immutable binding", r)
let wire = encode_lsp_message(create_response(1, h))
print(wire)Collect diagnostics for a file and publish them as a single notification:
use plugin lsp::{range, diagnostic, severity_error, severity_warning, publish_diagnostics, encode_lsp_message}
let diags = [
diagnostic(range(2, 0, 2, 5), "Undefined variable 'x'", severity_error()),
diagnostic(range(7, 4, 7, 9), "Unused binding", severity_warning()),
]
let notif = publish_diagnostics("file:///main.zolo", diags)
print(encode_lsp_message(notif))Decode an incoming wire message: read the header, then parse the JSON body:
use plugin lsp::{decode_lsp_header, parse_message}
let raw = "Content-Length: 52\r\n\r\n"
let header = decode_lsp_header(raw)
print("expecting {header["content_length"]} bytes")
let msg = parse_message(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#)
print("method: {msg["method"]}")Build a JSON-RPC request string
Builds a JSON-RPC 2.0 request string with the given id, method, and optional params table. Use this when sending requests that expect a response from the client or server.
use plugin lsp::{create_request}
let req = create_request(1, "textDocument/hover", #{"textDocument": #{"uri": "file:///main.zolo"}, "position": #{"line": 10, "character": 5}})
print(req)The id accepts any value, and the structural helpers compose into the params:
use plugin lsp::{create_request, position, encode_lsp_message}
let pos = position(3, 12)
let req = create_request("req-7", "textDocument/definition", #{"position": pos})
print(encode_lsp_message(req))Build a JSON-RPC response string
Builds a JSON-RPC 2.0 success response string with the given id and result table.
use plugin lsp::{create_response, hover}
let h = hover("**string** — built-in type")
let resp = create_response(1, h)
print(resp)Build a JSON-RPC notification string
Builds a JSON-RPC 2.0 notification string (no id field). Notifications do not expect a response.
use plugin lsp::{create_notification}
let notif = create_notification("textDocument/didOpen", #{"textDocument": #{"uri": "file:///main.zolo", "languageId": "zolo", "version": 1, "text": ""}})
print(notif)Build a JSON-RPC error response string
Builds a JSON-RPC 2.0 error response with the given request id, error code integer, and human-readable message.
use plugin lsp::{create_error_response}
let err = create_error_response(1, -32601, "Method not found")
print(err)Wrap JSON body with Content-Length header
Wraps a JSON string with the LSP wire-format header Content-Length: N\r\n\r\n. Use this before writing messages to stdout or a socket.
use plugin lsp::{create_notification, encode_lsp_message}
let body = create_notification("initialized", #{})
let wire = encode_lsp_message(body)
print(wire)Parse Content-Length from a raw header
Parses the Content-Length value from a raw LSP header string. Returns a table with a content_length integer field.
use plugin lsp::{decode_lsp_header}
let header = "Content-Length: 128\r\n\r\n"
let parsed = decode_lsp_header(header)
print("Body length: {parsed["content_length"]}")Create an LSP Position object
Creates an LSP Position object with zero-based line and character fields.
use plugin lsp::{position}
let pos = position(10, 5)Create an LSP Range object
Creates an LSP Range object with start and end position fields.
use plugin lsp::{range}
let r = range(10, 0, 10, 20)A range is just two positions, so it slots directly into a text_edit:
use plugin lsp::{range, text_edit}
let edit = text_edit(range(0, 0, 0, 3), "let")
print(edit["newText"])Create an LSP Diagnostic object
Creates an LSP Diagnostic object. Use severity_error(), severity_warning(), etc. to get the correct severity integer.
use plugin lsp::{range, diagnostic, severity_error}
let r = range(5, 0, 5, 10)
let diag = diagnostic(r, "Undefined variable 'x'", severity_error())Create an LSP CompletionItem object
Creates an LSP CompletionItem object. kind is an LSP completion kind integer (e.g. 1=Text, 2=Method, 3=Function, 6=Variable). detail and insert_text are optional.
use plugin lsp::{completion_item}
let item = completion_item("print", 3, "fn print(value: any)", "print($1)")The trailing two arguments are optional — omit them for a bare label:
use plugin lsp::{completion_item, create_response}
let items = [
completion_item("let", 14),
completion_item("match", 14),
]
let resp = create_response(5, items)
print(resp)Create an LSP Hover result object
Creates an LSP Hover result with markdown contents. The optional range highlights the hovered token in the editor.
use plugin lsp::{hover, range, create_response}
let r = range(2, 4, 2, 9)
let h = hover("**let** — declares an immutable binding", r)
let resp = create_response(42, h)Create an LSP DocumentSymbol object
Creates an LSP DocumentSymbol object. kind is an LSP symbol kind integer (e.g. 12=Function, 13=Variable). Both range and selection_range are LSP Range tables.
use plugin lsp::{range, document_symbol}
let full = range(0, 0, 10, 1)
let name_range = range(0, 3, 0, 8)
let sym = document_symbol("myFn", 12, full, name_range)Create an LSP TextEdit object
Creates an LSP TextEdit object describing a text replacement at range with new_text.
use plugin lsp::{range, text_edit}
let r = range(3, 0, 3, 5)
let edit = text_edit(r, "hello")Parse a JSON string into a table
Parses a raw JSON string into a Zolo table. Use this to decode incoming LSP messages received over stdin or a socket.
use plugin lsp::{parse_message}
let msg = parse_message(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#)
print("Method: {msg["method"]}")Nested objects decode into nested tables you can dig into by key:
use plugin lsp::{parse_message}
let msg = parse_message(r#"{"method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///a.zolo"}}}"#)
print("opened: {msg["params"]["textDocument"]["uri"]}")Returns diagnostic severity 1 (Error)
Returns the LSP diagnostic severity constant for Error (value 1).
use plugin lsp::{severity_error}
print(severity_error())Returns diagnostic severity 2 (Warning)
Returns the LSP diagnostic severity constant for Warning (value 2).
use plugin lsp::{severity_warning}
print(severity_warning())Returns diagnostic severity 3 (Information)
Returns the LSP diagnostic severity constant for Information (value 3).
use plugin lsp::{severity_info}
print(severity_info())Returns diagnostic severity 4 (Hint)
Returns the LSP diagnostic severity constant for Hint (value 4).
use plugin lsp::{severity_hint}
print(severity_hint())Build a publishDiagnostics notification
Builds a textDocument/publishDiagnostics notification JSON string containing the given uri and array of diagnostic tables.
use plugin lsp::{range, diagnostic, severity_warning, publish_diagnostics, encode_lsp_message}
let r = range(0, 0, 0, 5)
let diag = diagnostic(r, "Unused variable", severity_warning())
let notif = publish_diagnostics("file:///main.zolo", [diag])
let wire = encode_lsp_message(notif)
print(wire)