Search code examples
vscode-extensions

Invoke `editor.action.peekLocations` command from a code lens provided by a language server


I am trying to make a language server to create a code lens that opens an overlay to other files in VS Code. I tried the SO answer to How to implement overlay in VS Code Extension? (using the peekLocations command documented here) but in my own attempts I keep getting this unhelpful error when I click on the code lens:

argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true

Perhaps there is an issue in LSP implementation I use (the OCaml lsp library), but to make this question language-agnostic, here is the relevant JSON response from my server that creates the code lens:

[Trace - 7:50:16 PM] Received response 'textDocument/codeLens - (12)' in 10ms.
[
    {
        "command": {
            "arguments": [
                "file:///home/test/testfile.test",
                {
                    "character": 3,
                    "line": 9
                },
                [
                    {
                        "range": {
                            "end": {
                                "character": 2,
                                "line": 1
                            },
                            "start": {
                                "character": 1,
                                "line": 1
                            }
                        },
                        "uri": "file:///home/test/testfile2.test"
                    },
                    {
                        "range": {
                            "end": {
                                "character": 2,
                                "line": 1
                            },
                            "start": {
                                "character": 1,
                                "line": 1
                            }
                        },
                        "uri": "file:///home/test/testfile2.test"
                    },
                    {
                        "range": {
                            "end": {
                                "character": 2,
                                "line": 1
                            },
                            "start": {
                                "character": 1,
                                "line": 1
                            }
                        },
                        "uri": "file:///home/test/testfile2.test"
                    }
                ],
                "gotoAndPeek"
            ],
            "command": "editor.action.peekLocations",
            "title": "test"
        },
        "range": {
            "end": {
                "character": 9,
                "line": 9
            },
            "start": {
                "character": 3,
                "line": 9
            }
        }
    }
]

What is wrong with the "arguments" field, or anything else in this response?


Solution

  • The answer is that the peekLocations function expects objects of specific types (Uri, Position, Location[]), whereas when an LSP puts a command in a code lens, the arguments are JSON values (strings, arrays, and dictionaries) which are thus the wrong type. The workaround I found is to define my own command as a wrapper that decodes the JSON values into the expected objects before passing them to peekLocations.

    type RawPosition = {line: number, character: number}
    type RawRange = {start: RawPosition, end: RawPosition}
    type RawLocation = {uri: string, range: RawRange}
    
    const mkPosition = (raw : RawPosition) => new vscode.Position(raw.line, raw.character)
    const mkRange = (raw : RawRange) => new vscode.Range(mkPosition(raw.start), mkPosition(raw.end))
    const mkLocation = (raw : RawLocation) => new vscode.Location(Uri.parse(raw.uri), mkRange(raw.range))
    
    // New command "myextension.peekLocations" as a wrapper around "editor.action.peekLocations"
    const disposable = vscode.commands.registerCommand(context, "myextension.peekLocations", async (rawUri, rawPosition, rawLocations) => {
        const uri = Uri.parse(rawUri)
        const position = mkPosition(rawPosition)
        const locations = rawLocations.map(mkLocation)
        await vscode.commands.executeCommand("editor.action.peekLocations", uri, position, locations, "peek")
    });
    context.subscriptions.push(disposable);