Dev - Pharo Language Server

I have recently developed a VSCode extension for Pharo. It uses the Language Server Protocol and the Debug Adapter Protocol. You can download it from GitHub! Here, I will present the Pharo implementation for the LSP part and how to extend it.

I first worked on the implementation of LSP in Pharo. LSP protocol is based on the JSON-RPC protocol. Hopefully, this protocol is already implemented in Pharo thanks to the incredible work of Julien Delplanque.

Project Package Architecture

The LSP implementation is done in a main package PharoLanguageServer. Then, the package is subdivided into 5 sub-packages: Uncategorized (core), Structure, Structure-Capabilities, Structure-Completion, and Structure-Signature.

Project startup

Here, we will present how the project startup

sequenceDiagram activate VSCode VSCode->>Pharo: Start Pharo activate Pharo VSCode->>Pharo: Can you hear me? Pharo-->>VSCode: Yes! VSCode->>+Pharo: Initialized? Pharo->>-VSCode: Initialized! VSCode->>+Pharo: Capabilities? Pharo->>-VSCode: Capabilities! loop VSCode->>+Pharo: What about completion? Pharo->>-VSCode: Complete this text with this snippet end VSCode->>Pharo: I'm done Pharo->>VSCode: OK bye! deactivate VSCode deactivate Pharo

When a .st file is opened VSCode launch that vscode-pharo extension, which, in turn, starts the server by executing the following piece of code.

1| server | 2 3Transcript crShow: 'Run with vscode'. 4 5server := PLSServer new 6 "In the dev version" 7 debugMode: true; 8 yourself. 9 10server start.

When started:

  1. The PLSServer looks for its methods with the pragma jrpc to define the method that will be accessible by the extension. For instance, the following method is called when the client executes the method initialize.
1onInitializeTrace: trace processId: processId clientInfo: clientInfo rootPath: rootPath workspaceFolders: workspaceFolders capabilities: capabilities rootUri: rootUri 2 <jrpc: #initialize> 3 ^ PLSInitializeResult new
  1. It creates a TCP socket that listens to port 4000 (default value).
  2. The VSCode client connects to the server TCP port
  3. Client and server exchange their capabilities

Main loop

Once the VSCode client and the Pharo server are connected, the main loop of the protocol begins. Here, I will detail how information is handled by the server part. For information about the client, you should have a look at the VSCode documentation.

Receiving request

The server is always in listening mode, waiting for a request from the client. When it receives data, it first #extractRequestFrom the socket.

The extraction consists of waiting data from the client. A request consists of a header and content.

The header is first extracted by Pharo.

Content-Length: ...\r\n
\r\n

The server retrieves the value of the content-length. It allows us to create a String buffer with the correct size. Then, it extracts into the buffer the content.

1{ 2 "jsonrpc": "2.0", 3 "id": 1, 4 "method": "textDocument/didOpen", 5 "params": { 6 ... 7 } 8}

The content consists of the JSON-RPC protocol version, an idea used to identify the request, the method called, and the params for the methods. Handling the request, and dispatching to the correct method in the server is made by incredible JRPC implementation of Julien.

Handling request

When extracted, the request is dispatched to the method with the pragma corresponding to the jrpc called method with the parameter.

Example communication.

The method analysis the parameter, performs some Pharo code, and then answers with the expected LSP structure.

Example of code completion

To answer a completion request, the following implementation is used:

1textDocumentCompletionWithContext: context position: position textDocument: textDocument 2 <jrpc: #'textDocument/completion'> 3 | completionList completionTool | 4 completionTool := PLSCompletion new 5 source: ((self context textItem: (textDocument at: #uri)) at: #text); 6 position: position; 7 yourself. 8 completionList := PLSCompletionList new. 9 completionList completionItems: completionTool entries asArray. 10 ^ completionList

First, we create a PLSCompletion that has access to the source code, and the position in which a completion is required. Then, we create a PLSCompletionList, a structure defined in the LSP. Finally, we set the list of completion items with the entries given by our completion tool.

1PLSCompletion>>#entries 2 completionContext := CompletionContext 3 engine: PLSCompletionEngine new 4 class: nil 5 source: self source 6 position: self position. 7 ^ self completionContext entries 8 collectWithIndex: [ :entry :index | 9 PLSCompletionItem new 10 label: entry contents; 11 insertTextFormat: PLSInsertTextFormat snippet; 12 insertText: entry contents toPLSSnippet ; 13 kind: entry asPLSCompletionItemKind; 14 data: index; 15 yourself ]

The completion tool uses the existing Pharo tool CompletionContext for the completion. We created a specific engine named PLSCompletionEngine that extends the default CompletionEngine of Pharo, and defines the context as scripting.

How to extend and improve the project

There is still a lot of work to do to improve the Pharo Language Server. Using the existing architecture, it is easy to improve the code. Please consider adding your next super feature or creates issues so we can prioritize our work.

The next blog post will detail how to extend the Debug Adapter Protocol Pharo implementation, and another will present user story with the extension 🚀