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.
Let me explain further. So, generally, three main problems exist before LSP:
LSP fixes all of these issues. It better integrates and adopts the extensions, and it also helps spread task loads more evenly.
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:
The biggest part is the language function, which is a part that can also be implemented through local providers and other methods.
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.
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:
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.
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!
Louis Liu - August 27, 2019
Louis Liu - August 27, 2019
Louis Liu - August 27, 2019
Alibaba F(x) Team - June 20, 2022
Louis Liu - August 26, 2019
淘系技术 - November 4, 2020
A low-code development platform to make work easier
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreMore Posts by Louis Liu