×
Community Blog Quick Start to VSCode Plug-Ins: Language Server Protocol (LSP)

Quick Start to VSCode Plug-Ins: Language Server Protocol (LSP)

In this part of the tutorial series, we look at the language server protocol (LSP) can how it can help to solve some of the language extension pain points of VSCode and how you can use it with VSCode.

Quick Start to VSCode Plug-Ins: Language Server Protocol (LSP)

By Xulun

Language Server Protocol (LSP) is a protocol implemented by VSCode to solve some of the language extension pain points. LSP is a JSON RPC-based protocol. The figures below give a bit of an idea about this.

1

Let me explain further. So, generally, three main problems exist before LSP:

  1. Language-related extensions are written in the native language they come in, and cannot be easily integrated into plug-ins as a result. This is a problem because there are a large number of languages these days.
  2. Task loads related to language scanning is very CPU-intensive, which makes it so bad that it's better to run the work in a separate process or even on a remote server, rather than in VSCode.
  3. As shown on the left of the figure above, in the absence of a protocol, each language service needs to adapt to multiple source-code editors. Similarly, each editor also requires various language services. This wastes a significant amount of resources.

LSP fixes all of these issues. It better integrates and adopts the extensions, and it also helps spread task loads more evenly.

Starting out with the LSP Protocol

Understand a bit about the LSP protocol, let's first look at an example first:

Content-Length: ...\r\n
\r\n
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "textDocument/didOpen",
    "params": {
        ...
    }
}

jsonrpc is the header of the JSON-RPC protocol. Remember again that LSP is a JSON RPC-based protocol. LSP mainly defines the method and params.

The Request is sent from the server to the client, and the client returns the Response. Then, the client initiates the Notification. The following figure shows the features supported by LSP currently:

2

The biggest part is the language function, which is a part that can also be implemented through local providers and other methods.

Let's Look into Lifecycle Management

The lifecycle of the server starts when the client sends an initialize request. The load is an InitializeParameter object:

interface InitializeParams {
    /**
     * The process Id of the parent process that started
     * the server. Is null if the process has not been started by another process.
     * If the parent process is not alive then the server should exit (see exit notification) its process.
     */
    processId: number | null;

    /**
     * The rootPath of the workspace. Is null
     * if no folder is open.
     *
     * @deprecated in favour of rootUri.
     */
    rootPath?: string | null;

    /**
     * The rootUri of the workspace. Is null if no
     * folder is open. If both `rootPath` and `rootUri` are set
     * `rootUri` wins.
     */
    rootUri: DocumentUri | null;

    /**
     * User provided initialization options.
     */
    initializationOptions?: any;

    /**
     * The capabilities provided by the client (editor or tool)
     */
    capabilities: ClientCapabilities;

    /**
     * The initial trace setting. If omitted trace is disabled ('off').
     */
    trace?: 'off' | 'messages' | 'verbose';

    /**
     * The workspace folders configured in the client when the server starts.
     * This property is only available if the client supports workspace folders.
     * It can be `null` if the client supports workspace folders but none are
     * configured.
     *
     * Since 3.6.0
     */
    workspaceFolders?: WorkspaceFolder[] | null;
}

After this, the server returns the capabilities of the server:

interface InitializeResult {
    /**
     * The capabilities the language server provides.
     */
    capabilities: ServerCapabilities;
}

The definition of ServerCapabilities is as follows. Note that it mainly corresponds to two types of APIs, which are workspace and textDocument, which are shown below:

interface ClientCapabilities {
    /**
     * Workspace specific client capabilities.
     */
    workspace?: WorkspaceClientCapabilities;

    /**
     * Text document specific client capabilities.
     */
    textDocument?: TextDocumentClientCapabilities;

    /**
     * Experimental client capabilities.
     */
    experimental?: any;
}

After receiving the InitializeResult, the client returns an initialized message for confirmation based on the three-way handshake principle. At this point, the lifecycle of a server-client communication has been established.

Implementing the LSP Protocol

In addition to the detailed description of the entire protocol, Microsoft has also prepared the LSP SDK for us. The source code is available at: https://github.com/microsoft/vscode-languageserver-node

We first explain the usage of LSP SDK on the server aspect. We can do so with the following LSP function:

  • createConnection

The server first needs to obtain a Connection object, and create a Connection through the createConnection function provided by vscode-languageserver.

let connection = createConnection(ProposedFeatures.all);

The LSP message is encapsulated in Connection. For example:

        onInitialize: (handler) => initializeHandler = handler,
        onInitialized: (handler) => connection.onNotification(InitializedNotification.type, handler),
        onShutdown: (handler) => shutdownHandler = handler,
        onExit: (handler) => exitHandler = handler,
...
        onDidChangeConfiguration: (handler) => connection.onNotification(DidChangeConfigurationNotification.type, handler),
        onDidChangeWatchedFiles: (handler) => connection.onNotification(DidChangeWatchedFilesNotification.type, handler),
...
        onDidOpenTextDocument: (handler) => connection.onNotification(DidOpenTextDocumentNotification.type, handler),
        onDidChangeTextDocument: (handler) => connection.onNotification(DidChangeTextDocumentNotification.type, handler),
        onDidCloseTextDocument: (handler) => connection.onNotification(DidCloseTextDocumentNotification.type, handler),
        onWillSaveTextDocument: (handler) => connection.onNotification(WillSaveTextDocumentNotification.type, handler),
        onWillSaveTextDocumentWaitUntil: (handler) => connection.onRequest(WillSaveTextDocumentWaitUntilRequest.type, handler),
        onDidSaveTextDocument: (handler) => connection.onNotification(DidSaveTextDocumentNotification.type, handler),

        sendDiagnostics: (params) => connection.sendNotification(PublishDiagnosticsNotification.type, params),
...
        onHover: (handler) => connection.onRequest(HoverRequest.type, handler),
        onCompletion: (handler) => connection.onRequest(CompletionRequest.type, handler),
        onCompletionResolve: (handler) => connection.onRequest(CompletionResolveRequest.type, handler),
        onSignatureHelp: (handler) => connection.onRequest(SignatureHelpRequest.type, handler),
        onDeclaration: (handler) => connection.onRequest(DeclarationRequest.type, handler),
        onDefinition: (handler) => connection.onRequest(DefinitionRequest.type, handler),
        onTypeDefinition: (handler) => connection.onRequest(TypeDefinitionRequest.type, handler),
        onImplementation: (handler) => connection.onRequest(ImplementationRequest.type, handler),
        onReferences: (handler) => connection.onRequest(ReferencesRequest.type, handler),
        onDocumentHighlight: (handler) => connection.onRequest(DocumentHighlightRequest.type, handler),
        onDocumentSymbol: (handler) => connection.onRequest(DocumentSymbolRequest.type, handler),
        onWorkspaceSymbol: (handler) => connection.onRequest(WorkspaceSymbolRequest.type, handler),
        onCodeAction: (handler) => connection.onRequest(CodeActionRequest.type, handler),
        onCodeLens: (handler) => connection.onRequest(CodeLensRequest.type, handler),
        onCodeLensResolve: (handler) => connection.onRequest(CodeLensResolveRequest.type, handler),
        onDocumentFormatting: (handler) => connection.onRequest(DocumentFormattingRequest.type, handler),
        onDocumentRangeFormatting: (handler) => connection.onRequest(DocumentRangeFormattingRequest.type, handler),
        onDocumentOnTypeFormatting: (handler) => connection.onRequest(DocumentOnTypeFormattingRequest.type, handler),
        onRenameRequest: (handler) => connection.onRequest(RenameRequest.type, handler),
        onPrepareRename: (handler) => connection.onRequest(PrepareRenameRequest.type, handler),
        onDocumentLinks: (handler) => connection.onRequest(DocumentLinkRequest.type, handler),
        onDocumentLinkResolve: (handler) => connection.onRequest(DocumentLinkResolveRequest.type, handler),
        onDocumentColor: (handler) => connection.onRequest(DocumentColorRequest.type, handler),
        onColorPresentation: (handler) => connection.onRequest(ColorPresentationRequest.type, handler),
        onFoldingRanges: (handler) => connection.onRequest(FoldingRangeRequest.type, handler),
        onExecuteCommand: (handler) => connection.onRequest(ExecuteCommandRequest.type, handler),

All messages in the protocol are encapsulated. Now, let's look at the onInitalize function.

  • onInitialize

After creating a Connection object through createConnection, we can call connection.listen() to listen to the client. But before implementing listening, we need to set up the callback function that processes listening events. For this, the first will be the onInitialize, which is a function that processes the initialize message. As discussed before in this tutorial, the main task is to inform the client of the capabilities of the server:

connection.onInitialize((params: InitializeParams) => {
    let capabilities = params.capabilities;

    return {
        capabilities: {
            textDocumentSync: documents.syncKind,
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});

According to the three-way handshake principle, the client will also return the initialized notification as the notification. The server can perform some initialization by processing the returned value of this notification. Consider the following example to see how this works:

connection.onInitialized(() => {
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
            connection.console.log('Workspace folder change event received.');
        });
    }
});

From the above example, you can see that the three-way handshake kind of principle in action. The client returns the initialized notification for the notification. Okay, so that's the basics of some ways you can use LSP with VSCode!

0 0 0
Share on

Louis Liu

7 posts | 0 followers

You may also like

Comments