https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
Content-Lenght
Content-Type
; default: application/vscode-jsonrpc;charset=utf-8
jsonrpc
– version of JSON-RPC used, always equal to 2.0Content-Length: ...\r\n
\r\n
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/completion",
"params": {
...
}
}
id
method
params
Content-Length: ...\r\n
\r\n
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/completion",
"params": {
...
}
}
id
result
error
Content-Length: ...\r\n
\r\n
{
"jsonrpc": "2.0",
"id": 1,
"result": {
...
}
}
method
params
Content-Length: ...\r\n
\r\n
{
"jsonrpc": "2.0",
"method": "initialized",
"params": {
...
}
}
Notifications:
(Leveraging kotlin-lsp and LSP4J)Sources: https://github.com/S-furi/lsp-presentation/tree/main/code/kt-lsp-example
Accomplished by means of Sockets input and output streams
val socket = Socket("127.0.0.1", 9999)
val launcher =
LSPLauncher.createClientLauncher(
languageClient,
socket.inputStream,
socket.outputStream
)
launcher.startListening()
val lspServer = launcher.remoteProxy
A Workspace is the location of a Kotlin project. As of today, kotlin-lsp (https://github.com/Kotlin/kotlin-lsp) supports Gradle Kotlin/JVM projects only, meaning that valid workspaces are only kotlin projects with such limitations. Due to this we could not use it with standalone files.
In order to register a workspace, we must specify project’s URI
and attach it to the initial request to be sent to the server.
projectFolders = listOf(WorkspaceFolder("file://$projectPath", projectName))
val params = InitializeParams().apply { workspaceFolders = projectFolders }
Once we successfully launch a client instance, we must start the initialization procedure before any other request. In this phase
we specify the WorkspaceFolder
and ClientCapabilities
(later), upon which the server will respond with his capabilities.
The initialization procedure can end only when, upon receiving server’s
response, the client sends an initialized
notification in order to signal
that it’s ready state.
fun initialize(cc: ClientCapabilities, wf: List<WorkSpaceFolder>): Future<Void> {
val params = InitializeParams().apply {
capabilities = cc
workspaceFolders = wf
}
return languageServer.initialize(params).thenCompose {
languageServer.initialized()
CompletableFuture.complete(null)
}
}
Client capabilities defines which operations will be available during client-server session. For this project, the most important capability is Completion
.
interface CompletionClientCapabilities {
dynamicRegistration?: boolean;
completionItem?: {
snippetSupport?: boolean;
commitCharactersSupport?: boolean;
documentationFormat?: MarkupKind[];
deprecatedSupport?: boolean;
preselectSupport?: boolean;
tagSupport?: { valueSet: CompletionItemTag[]; };
insertReplaceSupport?: boolean;
resolveSupport?: { properties: string[]; };
insertTextModeSupport?: { valueSet: InsertTextMode[]; };
labelDetailsSupport?: boolean;
};
completionItemKind?: { valueSet?: CompletionItemKind[]; };
contextSupport?: boolean;
insertTextMode?: InsertTextMode;
completionList?: { itemDefaults?: string[]; }
}
A lot of configurations… mostly related on how the editor shows and insert received completions options.
Completion
capabilities are under the textDocument
set of capabilities, along with others like Hover
, Signature
, Definition
, etc.
val txtDocCap = TextDocumentCapabilities().apply {
// set completion configurations (could use also have used `apply` here too)
val cmplCap = CompletionCapabilities()
cmplCap.setContextSupport(true)
cmplCap.setCompletionItem(CompletionItemCapabilities(snippetSupport = true))
// cmplCap.set...
completion = cmplCap
}
val capabilities = CompletionCapabilities.apply {
textDocument = txtDocCap
}
In order to make the editor signals the language server to:
we must use the methods didOpen
, didChange
and didClose
respectively.
To keep track of files and their contents, we use TextDocumentIdentifier
:
simply a container for their URI and a version number if clients support it
(not necessarily incremental).
We create an instance of TextDocumentItem
with its URI, version number, language id and it’s content.
By sending the notification textDocumentService/didOpen
, the server will now start tracking and synching the document.
val content = Files.readString(Paths.get(uri))
val params = DidOpenTextDocumentParams(
TextDocumentItem(uri.toString(), "kotlin", 1, content)
)
languageServer.textDocumentService.didOpen(params)
Two different approaches in informing the server about changes: incremental, meaning that we send diffs (new contents and their positions), or full where the entire, updated document content is sent to the server. In the latter case, we could simply do:
fun changeDocument(uri: URI, newContent: String) {
val params = DidChangeTextDocumentParams(
VersionedTextDocumentIdentifier(uri.toString(), 1),
listOf(TextDocumentContentChangeEvent(newContent)),
)
languageServer.textDocumentService.didChange(params)
}
Changes are represented as a list of TexDocumentContentChangeEvent
: such events can define insertions/modifications/deletions in case the strategy for updating the fil is incremental
(which must set in initial client capabilities, otherwise full
is the default).
The closing notification just simply requires the URI of the document
val params = DidCloseTextDocumentParams(TextDocumentIdentifier(uri.toString()))
languageServer.textDocumentService.didClose(params)
documentText/Completion
When one of these three events occur, we can trigger a completion specifying the URI of the file and the cursor line and character position,
and retrieve a result of type List<CompletionItem>
or CompletionList
depending on whether current completion is incomplete or not.
fun getCompletion(
uri: URI,
position: Position,
triggerKind: CompletionTriggerKind = CompletionTriggerKind.Invoked
): Future<Completions> {
val context = CompletionContext(triggerKind)
val params = CompletionParams(TextDocumentIdentifier(uri.toString()), position, context)
return languageServer.textDocumentService.completion(params)
?: CompletableFuture.completedFuture(Either.forLeft(emptyList()))
}