-
Notifications
You must be signed in to change notification settings - Fork 182
Language Server Protocol Support in Brackets
The Language Server protocol is used between a tool (the client) and a language smartness provider (the server) to integrate features like autocomplete, go to definition, find all references and alike into the tool.
For implementing tooling support for any language in Brackets, we need developers who are both
- Brackets experts
- Language experts
LSP decouples these two:
- Language Server can be developed independently (by the language experts)
- Can be plugged into any editor having LSP framework (developed by Brackets experts) as an extension.
Source: https://microsoft.github.io/language-server-protocol/overview
Current LSP infrastructure supports
- Code completion
- Jump to definition
- Function signature
- Diagnostics/Linting
- Find Symbols
- Find References
Future Work
- Support for other capabilities like snippet, hover etc by LSP
See specification for reference: https://microsoft.github.io/language-server-protocol/specification
Brackets itself has two primary components:
- Brackets-Shell (Customized CEF Client which allows browser capabilities)
- Node Server Process (Spawned by Brackets-Shell on startup which provides a separate node runtime)
The LSP framework in Brackets allows us to create a client for the language server in the node runtime and then seamlessly communicate with it using an interface instance from within Brackets runtime (CEF).
The actual LanguageClient is hosted within the node context. This client is responsible for spawning the language server, establishing communication with the server and then acting as a bridge between Brackets and the server for the two-way requests and notifications.
Once the server is spawned and LanguageClient created, we get an interface object that allows us to send and receive requests and notifications to and from the server using the LanguageClient.
This interface is an instance of LanguageClientWrapper.
The entire communications logic is abstracted and is Promise based.
Refer to the control flow diagram below to understand how LSP works in Brackets.
Pre-requisites: How to write Brackets Extensions
The Language Client in Brackets abstracts the communication between the Extension context in Brackets and Node, streamlining the connection between the Extension and the Language Server.
The Extension usually would have three layers:
- Brackets Context (lies inside the CEF/Shell)
- Language Client Context (lies as a Node Domain)
- Language Server (Separate process) The communication between Language Client and the Server happens through a node module.
The synchronization between the Language Client and Brackets is handled by an object which interfaces with the Language Client and acts as a wrapper called LanguageClientWrapper.
Bare minimally the developer will need two files to create a LanguageClient extension:
-
main.js (extension entry point)
-
Initiates client for a Language Server (provide path of client.js)
-
Manage the lifecycle of Language Server, using the client.
-
Interact with Brackets’ core and Language Server to provide tooling.
See reference here
-
-
client.js
-
Contains Information specific to launching a Language Server
-
Instantiates a LanguageClient that interacts with a Language Server
-
Handles any other to-and-fro of information between Brackets and Node which might be required before spawning the server, like ‘runtime’ path, specific options etc.
-
Infrastructure provided as part of LSP.
See reference here
-
- client.js: This file describes how to start a language server and works as a bridge between the Language Server and Brackets Context. Any customizations that are required from the Brackets Context are handled here:
- Load the LanguageClient Module.
var LanguageClient = require(global.LanguageClientInfo.languageClientPath).LanguageClient;
-
Define the serverOptions. serverOptions tells the client how to start the server. These options can be a JSON object or a function. Refer here and here to know various ways of defining the serverOptions.
serverOptions can be defined in three ways:
- Runtime (if the server is not a node module) or Module (node runtime) JSON
- Function
- Command JSON
Runtime, Module & Command are abstractions for node's spawn and fork functions. Refer spawn & fork to understand the option parameters.
//Sample Runtime based option (goes through spawn in case runtime is specified and fork otherwise)
//command line format: [runtime] [execArgs] [module] [args (with communication args)] (with options[env, cwd])
serverOptions = {
runtime: process.execPath, //Path to node but could be anything, like php or perl. No need to specify this if the module is a node module
module: "main.js",
args: [
"--server-args" //module args
], //Arguments to process
options: {
cwd: serverPath, //The current directory where main.js is located
env: newEnv, //The process will be started CUSTOMENVVARIABLE in its environment
execArgv: [
"--no-warnings",
"--no-deprecation" //runtime executable args
]
},
communication: "ipc"
};
//Sample function based options (executed directly)
serverOptions = function () {
return new Promise(function (resolve, reject) {
var serverProcess = cp.spawn(process.execPath, [
"main.js",
"--stdio" //Have to add communication args manually
], {
cwd: serverPath
});
if (serverProcess && serverProcess.pid) {
resolve({
process: serverProcess
});
} else {
reject("Couldn't create server process");
}
});
};
//Sample command based options (goes through spawn)
//command line format: [command] [args] (with options[env, cwd])
serverOptions = {
command: process.execPath, //Path to executable, mostly runtime
args: [
"--no-warnings",
"--no-deprecation",
"main.js",
"--stdio", //Have to add communication args manually
"--server-args"
], //Arguments to process, ORDER WILL MATTER
options: {
cwd: serverPath,
env: newEnv //The process will be started CUSTOMENVVARIABLE in its environment
}
};
- Set the LanguageClient options.
var options = {
serverOptions : serverOptions
};
- Instantiate the LanguageClient in the init function.
function init(domainManager) {
client = new LanguageClient(clientName, domainManager, options); //Initiate a new LanguageClient with options
}
//or
function init(domainManager) {
client = new LanguageClient(clientName, domainManager); //Initiate a new LanguageClient
}
client.setOptions(options); //We generally load the options later when the options
// require some information from the Brackets context that is not immediately available in init method.
//See https://github.com/brackets-cont/brackets/blob/master/test/spec/LanguageTools-test-files/clients/CommunicationTestClient/client.js
- main.js:
- Load the LanguageTools module in main.js
var LanguageTools = brackets.getModule("languageTools/LanguageTools");
- Load the LanguageClient described in client.js through the initiateToolingService API
- clientName (name of client)
- clientFilePath (absolute path of the LanguageClient)
- languageIdsArray (Array of languageIds describing the languages being supported by the LanguageClient)
var client = null;
AppInit.appReady(function () {
LanguageTools.initiateToolingService(clientName, clientFilePath, languageIdsArray).done(function (_client) {
client = _client; //LanguageClientWrapper object
});
});
- Now that we have the client, it can be used to interact with the server.
- Server Lifecycle APIs
//Start with options
client.start({
rootPath: projectPath,
capabilities ? : capabilities //parameters marked with '?' are optional
});
//Stop a client
client.stop();
//Restart a client
client.restart({
rootPath: projectPath,
capabilities ? : capabilities
});
- Message Format for Requests and Notifications:
You can communicate with the server by using the client API and send a proper JSON RPC message.
This message can have two formats:
- brackets (default)
- lsp (as defined in the specification)
//So you have two ways of sending the same message to the server:
//In Brackets Format
client.requestHints({ //Converted internally to the LSP format
filePath: docPath,
cursorPos: pos
});
//or
//In LSP Format as defined here: https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
//This is for advanced developers who understand the protocol and would like to customize Brackets tooling as per their need.
//Basically you can use the JSON as described by the Specification,
//with an additional key 'format' telling brackets not convert the message.
client.requestHints({
format: 'lsp'
textDocument: {
uri: fileUri
},
position: position //LSP format
});
- Events APIs
//Notify server of file open event
client.notifyTextDocumentOpened({
languageId: languageId,
filePath: (doc.file._path || doc.file.fullPath),
fileContent: doc.getText()
});
//Notify server of file closed event
client.notifyTextDocumentClosed({
filePath: (previous.document.file._path || previous.document.file.fullPath)
});
//Notify server of project change
client.notifyProjectRootsChanged({
foldersAdded: [this.currentProject],
foldersRemoved: [this.previousProject]
});
//Notify server of file save
client.notifyTextDocumentSave({
filePath: (doc.file._path || doc.file.fullPath)
});
//Notify server of file change
client.notifyTextDocumentChanged({
filePath: (doc.file._path || doc.file.fullPath),
fileContent: doc.getText()
});
- Server request APIs
//Client request for Language Server
//https://microsoft.github.io/language-server-protocol/specification
//All request return $.Deferred() promise.
//https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
client.requestHints({
filePath: docPath1,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#completionItem_resolve
client.getAdditionalInfoForHint({
hintItem: hintItem
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp
client.requestParameterHints({
filePath: docPath2,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_definition
client.gotoDefinition({
filePath: docPath2,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation
client.gotoImplementation({
filePath: docPath2,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration
client.gotoDeclaration({
filePath: docPath2,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_references
client.findReferences({
filePath: docPath2,
cursorPos: pos
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
client.requestSymbolsForDocument({
filePath: docPath2
});
//https://microsoft.github.io/language-server-protocol/specification#workspace_symbol
client.requestSymbolsForWorkspace({
query: query
});
- Server Event APIs
//Server Events for Language Server
//https://microsoft.github.io/language-server-protocol/specification#window_logMessage
client.addOnLogMessage(function (params) {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#window_showMessage
client.addOnShowMessage(function (params) {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#telemetry_event
client.addOnTelemetryEvent(function (params) {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics
client.addOnCodeInspection(function (params) {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#client_registerCapability
client.onDynamicCapabilityRegistration(function () {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#client_unregisterCapability
client.onDynamicCapabilityUnregistration(function () {
//do something
});
//https://microsoft.github.io/language-server-protocol/specification#window_showMessageRequest
client.onShowMessageWithRequest(function () {
//return something, can be a promise.
});
//https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWorkspaceFolders
client.onProjectFoldersRequest(function () {
//return something, can be a promise.
});
//Can be any notification mentioned above, or not yet implemented in Brackets Core
client.onCustomNotification(type, function (params) {
//do something
});
//Can be any request mentioned above, or not yet implemented in Brackets Core
client.onCustomRequest("custom/serverRequest", function (params) {
//return something, can be a promise.
});
//Can be used to extend the server and handle Brackets specific events
//https://github.com/brackets-cont/brackets/blob/master/test/spec/LanguageTools-test.js#L1482
client.addOnCustomEventHandler("triggerDiagnostics", function () {
//do something
});
Notes: Refer to Sample implementations for Requests and Notifications for reference.
Additionally, we have created a reference adoption for the PHP language server which can be studied to understand how these APIs work within Brackets.