Recording Sessions

Important

This guide assumes that you have already configured your client to wrap your language server with the LSP Agent.

The lsp-devtools record command can be used to either record an LSP session to a file, SQLite database or print the received messages direct to the console. Running the lsp-devtools record command you should see a message like the following:

$ lsp-devtools record
Waiting for connection on localhost:8765...

once the agent connects, the record command will by default, start printing all LSP messages to the console, with the JSON contents pretty printed.

../../_images/record-example.svg

Example Commands

Here are some example usages of the record command that you may find useful.

Capture the client’s capabilities

The following command will save to a JSON file only the client’s info and lsprotocol.types.ClientCapabilities sent during the initialize request - useful for adding clients to pytest-lsp! 😉

lsp-devtools record -f '{{"clientInfo": {.params.clientInfo}, "capabilities": {.params.capabilities}}}' --to-file <client_name>_v<version>.json

Format and show any window/logMessages

This can be used to replicate the Output log panel in VSCode in editors that do not provide a similar facility.

lsp-devtools record -f "{.params.type|MessageType}: {.params.message}"
../../_images/record-log-messages.svg

Read on for a comprehensive overview of all the available command line options.

Connection Options

By default, the LSP agent and other commands will attempt to connect to each other on localhost:8765. The following options can be used to change this behavior

--host <host>

The host to bind to.

-p <port>, --port <port>

The port number to open the connection on.

Alternate Destinations

As well as printing to console, the record command supports a number of other output destinations.

--to-file <filename>

Saves all collected messages to a plain text file with each line representing a complete JSON-RPC message:

lsp-devtools record --to-file example.json

See here for example of the output produced by this command.

--to-sqlite <filename>

Save messages to a SQLite database:

lsp-devtools record --to-sqlite example.db

This database can then be opened in other tools like datasette, SQLite Browser or even lsp-devtools own LSP Inspector.

DB Schema

Here is the schema currently used by lsp-devtools. Note: Except perhaps the base protocol table, this schema is not stable and may change between lsp-devtools releases.

-- Tables

-- We use a single table 'protocol' to store all messages sent between client and server.
-- Data within the table is then exposed through a number of SQL views, that parse out the
-- details relevant to that view.
CREATE TABLE IF NOT EXISTS protocol (
    session TEXT,
    timestamp REAL,
    source TEXT,

    id TEXT NULL,
    method TEXT NULL,
    params TEXT NULL,
    result TEXT NULL,
    error TEXT NULL
);

-- Views

-- Requests
CREATE VIEW IF NOT EXISTS requests AS
SELECT
    client.session,
    client.timestamp,
    (server.timestamp - client.timestamp) * 1000 as duration,
    client.id,
    client.method,
    client.params,
    server.result,
    server.error
FROM protocol as client
INNER JOIN protocol as server ON
    client.session = server.session AND
    client.id = server.id AND
    client.params IS NOT NULL AND
    (
        server.result IS NOT NULL OR
        server.error IS NOT NULL
    );

-- Notifications
CREATE VIEW IF NOT EXISTS notifications AS
SELECT
    rowid,
    session,
    timestamp,
    source,
    method,
    params
FROM protocol
WHERE id is NULL;

-- Sessions
CREATE VIEW IF NOT EXISTS sessions AS
SELECT
    session,
    timestamp,
    json_extract(params, "$.clientInfo.name") as client_name,
    json_extract(params, "$.clientInfo.version") as client_version,
    json_extract(params, "$.rootUri") as root_uri,
    json_extract(params, "$.workspaceFolders") as workspace_folders,
    params,
    result
FROM requests WHERE method = 'initialize';

-- Log Messages
CREATE VIEW IF NOT EXISTS logMessages AS
SELECT
    rowid,
    session,
    timestamp,
    json_extract(params, "$.type") as type,
    json_extract(params, "$.message") as message
FROM protocol
WHERE method = 'window/logMessage';
--save-output <filename>

Print to console as normal but additionally, the ouput will be saved into a text file using the export feature of rich’s Console object:

lsp-devtools record --save-output filename.{html,svg,txt}

Depending on the file extension used, this will save the output as plain text or rendered as an SVG image or HTML webpage - useful for generating screenshots for your documentation!

Filtering Messages

Once it gets going, the LSP protocol can generate a lot of messages! To help you focus on the messages you are interested in the record command provides the following options for selecting a subset of messages to show.

--message-source <source>

The following values are accepted

client

Only show messages sent from the client

server

Only show messages sent from the server

both (the default)

Show message sent from both client and server

--include-message-type <type>

Only show messages of the given type. This option can be used more than once to select multiple message types. The following values are accepted

request

Show only JSON-RPC request messages

response

Show only JSON-RPC response messages, matches responses containing either successful results or error codes.

result

Show only JSON-RPC response messages containing successful results

error

Show only JSON-RPC response messages that contain errors.

notification

Show only JSON-RPC notification messages

--include-method <method>

Only show messages with the given method name. This option can be used more than once to select multiple methods.

--exclude-message-type <type>

Like --include-message-type, but omit matches rather than showing them

--exclude-method <method>

Like --include-method, but omit matches rather than showing them

If multiple options from this list are used, they will be ANDed together, for example:

lsp-devtools record --message-source client \
                    --include-message-type request \
                    --include-message-type notification

will only show requests or notifications that have been sent by the client.

Formatting messages

Note

These options do not apply when using the --to-sqlite option.

-f <format>, --format-message <format>

Set the format string to use when formatting messages. By default, the record command will simply print the JSON contents of a message however, you can supply a custom format string to use instead.

Tip

Format strings are also a powerful filtering mechanism! - any messages that do not fit with the supplied format will not be shown

Format strings use the following syntax

Feedback Wanted!

We’re looking for feedback on this syntax, especially when it comes to formatting lists of items. Let us know by opening an issue if you have any thoughts or suggested improvements

Similar to Python’s Format String Syntax a pair of braces ({}) denote a placeholder where a value can be inserted. Inside the braces you can then select and the message field you want to be inserted using a dot-separated syntax that should feel familiar if you’ve ever used jq:

Message:
{
  "method": "textDocument/completion",
  "params": {
    "position": {"line": 1, "character": 2},
    "textDocument": {"uri": "file:///path/to/file.txt"},
  }
}

Format String:
"{.params.position.line}:{.params.position.character}"

Result:
1:2

The pipe symbol (|) can be used to pass the selected field to a formatter e.g. Position:

Message:
{
  "method": "textDocument/completion",
  "params": {
    "position": {"line": 1, "character": 2},
    "textDocument": {"uri": "file:///path/to/file.txt"},
  }
}

Format String:
"{.params.position|Position}"

Result:
1:2

See Formatters for details on all available formatters. Fields that contain an array of items can be accessed with square brackets ([]), by default items in an array will be separated by newlines when formatted:

Message:
{
  "result": {
    "items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
  }
}

Format String:
"{.result.items[].label}"

Result:
one
two
three

However, you can specify a custom separator inside the brackets:

Message:
{
  "result": {
    "items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
  }
}

Format String:
"{.result.items[\n- ].label}"

Result:
- one
- two
- three

The brackets also support Python’s standard list indexing rules:

Message:
{
  "result": {
    "items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
  }
}

Format String:                  Result:
"{.result.items[0].label}"      one
"{.result.items[-1].label}"     three
"{.result.items[0:2].label}"    "one\ntwo"

Finally, if you want to supply an index and adjust the separator you can separate them with the # symbol:

Message:
{
  "result": {
    "items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
  }
}

Format String:
"{.result.items[0:2#\n- ].label}"

Result:
- one
- two

Formatters

lsp-devtools provides the following formatters

json (default)

Renders objects as “pretty” JSON, equivalent to json.dumps(obj, indent=2)

json-compact

Renders objects as JSON with no additional formatting, equivalent to json.dumps(obj)

position

{"line": 1, "character": 2} will be rendered as 1:2

range

{"start": {"line": 1, "character": 2}, "end": {"line": 3, "character": 4}} will be rendered as 1:2-3:4

Additionally, any enum type can be used as a formatter, where numbers will be replaced with their corresponding name, for example:

Format String:
"{.type|MessageType}"

Value:          Result:
{"type": 1}     Error
{"type": 2}     Warning
{"type": 3}     Info
{"type": 4}     Log