User Guide

Language Server

The language server is responsible for managing the connection with the client as well as sending and receiving messages over the Language Server Protocol which is based on the Json RPC protocol.

Connections

pygls supports TCP, STDIO and WEBSOCKET connections.

TCP

TCP connections are usually used while developing the language server. This way the server can be started in debug mode separately and wait for the client connection.

Note

Server should be started before the client.

The code snippet below shows how to start the server in TCP mode.

from pygls.server import LanguageServer

server = LanguageServer('example-server', 'v0.1')

server.start_tcp('127.0.0.1', 8080)

STDIO

STDIO connections are useful when client is starting the server as a child process. This is the way to go in production.

The code snippet below shows how to start the server in STDIO mode.

from pygls.server import LanguageServer

server = LanguageServer('example-server', 'v0.1')

server.start_io()

WEBSOCKET

WEBSOCKET connections are used when you want to expose language server to browser based editors.

The code snippet below shows how to start the server in WEBSOCKET mode.

from pygls.server import LanguageServer

server = LanguageServer('example-server', 'v0.1')

server.start_ws('0.0.0.0', 1234)

Logging

Logs are useful for tracing client requests, finding out errors and measuring time needed to return results to the client.

pygls uses built-in python logging module which has to be configured before server is started.

Official documentation about logging in python can be found here. Below is the minimal setup to setup logging in pygls:

import logging

from pygls.server import LanguageServer

logging.basicConfig(filename='pygls.log', filemode='w', level=logging.DEBUG)

server = LanguageServer('example-server', 'v0.1')

server.start_io()

Overriding LanguageServerProtocol

If you have a reason to override the existing LanguageServerProtocol class, you can do that by inheriting the class and passing it to the LanguageServer constructor.

Custom Error Reporting

The default LanguageServer will send a window/showMessage notification to the client to display any uncaught exceptions in the server. To override this behaviour define your own report_server_error() method like so:

class CustomLanguageServer(LanguageServer):
    def report_server_error(self, error: Exception, source: Union[PyglsError, JsonRpcException]):
        pass

Handling Client Messages

Requests vs Notifications

Unlike a request, a notification message has no id field and the server must not reply to it. This means that, even if you return a result inside a handler function for a notification, the result won’t be passed to the client.

The Language Server Protocol, unlike Json RPC, allows bidirectional communication between the server and the client.

For the majority of the time, a language server will be responding to requests and notifications sent from the client. pygls refers to the handlers for all of these messages as features with one exception.

The Language Server protocol allows a server to define named methods that a client can invoke by sending a workspace/executeCommand request. Unsurprisingly, pygls refers to these named methods a commands.

Built-In Features

pygls comes with following predefined set of handlers for the following Language Server Protocol (LSP) features:

Note

Built-in features in most cases should not be overridden.

If you need to do some additional processing of one of the messages listed below, register a feature with the same name and your handler will be called immediately after the corresponding built-in feature.

Lifecycle Messages

  • The initialize request is sent as a first request from client to the server to setup their communication. pygls automatically computes registered LSP capabilities and sends them as part of the InitializeResult response.

  • The shutdown request is sent from the client to the server to ask the server to shutdown.

  • The exit notification is sent from client to the server to ask the server to exit the process. pygls automatically releases all resources and stops the process.

Text Document Synchronization

  • The textDocument/didOpen notification will tell pygls to create a document in the in-memory workspace which will exist as long as the document is opened in editor.

  • The textDocument/didChange notification will tell pygls to update the document text. pygls supports full and incremental document changes.

  • The textDocument/didClose notification will tell pygls to remove a document from the in-memory workspace.

Notebook Document Synchronization

  • The notebookDocument/didOpen notification will tell pygls to create a notebook document in the in-memory workspace which will exist as long as the document is opened in editor.

  • The notebookDocument/didChange notification will tell pygls to update the notebook document include its content, metadata, execution results and cell structure.

  • The notebookDocument/didClose notification will tell pygls to remove the notebook from the in-memory workspace.

Miscellanous

Registering Handlers

See also

It’s recommeded that you follow the tutorial before reading this section.

  • The feature() decorator is used to register a handler for a given LSP message.

  • The command() decorator is used to register a named command.

The following applies to both feature and command handlers.

Language servers using pygls run in an asyncio event loop. They asynchronously listen for incoming messages and, depending on the way handler is registered, apply different execution strategies to process the message.

Depending on the use case, handlers can be registered in three different ways:

Asynchronous Functions (Coroutines)

pygls supports python 3.8+ which has a keyword async to specify coroutines.

The code snippet below shows how to register a command as a coroutine:

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING)
async def count_down_10_seconds_non_blocking(ls, *args):
    # Omitted

Registering a feature as a coroutine is exactly the same.

Coroutines are functions that are executed as tasks in pygls’s event loop. They should contain at least one await expression (see awaitables for details) which tells event loop to switch to another task while waiting. This allows pygls to listen for client requests in a non blocking way, while still only running in the main thread.

Tasks can be canceled by the client if they didn’t start executing (see Cancellation Support).

Warning

Using computation intensive operations will block the main thread and should be avoided inside coroutines. Take a look at threaded functions for more details.

Synchronous Functions

Synchronous functions are regular functions which blocks the main thread until they are executed.

Built-in features are registered as regular functions to ensure correct state of language server initialization and workspace.

The code snippet below shows how to register a command as a regular function:

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING)
def count_down_10_seconds_blocking(ls, *args):
    # Omitted

Registering feature as a regular function is exactly the same.

Warning

Using computation intensive operations will block the main thread and should be avoided inside regular functions. Take a look at threaded functions for more details.

Threaded Functions

Threaded functions are just regular functions, but marked with pygls’s thread decorator:

# Decorator order is not important in this case
@json_server.thread()
@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING)
def count_down_10_seconds_blocking(ls, *args):
    # Omitted

pygls uses its own thread pool to execute above function in daemon thread and it is lazy initialized first time when function marked with thread decorator is fired.

Threaded functions can be used to run blocking operations. If it has been a while or you are new to threading in Python, check out Python’s multithreading and GIL before messing with threads.

Passing Language Server Instance

Using language server methods inside registered features and commands are quite common. We recommend adding language server as a first parameter of a registered function.

There are two ways of doing this:

  • ls (language server) naming convention

Add ls as first parameter of a function and pygls will automatically pass the language server instance.

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING)
def count_down_10_seconds_blocking(ls, *args):
    # Omitted
  • add type to first parameter

Add the LanguageServer class or any class derived from it as a type to first parameter of a function

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_BLOCKING)
def count_down_10_seconds_blocking(ser: JsonLanguageServer, *args):
    # Omitted

Using outer json_server instance inside registered function will make writing unit tests more difficult.

Communicating with the Client

Important

Most of the messages listed here cannot be sent until the LSP session has been initialized. See the section on the initiaiize request in the specification for more details.

In addition to responding to requests, there are a number of additional messages a server can send to the client.

Configuration

The workspace/configuration request is sent from the server to the client in order to fetch configuration settings from the client. Depending on how the handler is registered (see here) you can use the get_configuration() or get_configuration_async() methods to request configuration from the client:

  • asynchronous functions (coroutines)

    # await keyword tells event loop to switch to another task until notification is received
    config = await ls.get_configuration(
        WorkspaceConfigurationParams(
            items=[
                ConfigurationItem(scope_uri='doc_uri_here', section='section')
            ]
        )
    )
    
  • synchronous functions

    # callback is called when notification is received
    def callback(config):
        # Omitted
    
    params = WorkspaceConfigurationParams(
        items=[
            ConfigurationItem(scope_uri='doc_uri_here', section='section')
        ]
    )
    config = ls.get_configuration(params, callback)
    
  • threaded functions

    # .result() will block the thread
    config = ls.get_configuration(
        WorkspaceConfigurationParams(
            items=[
                ConfigurationItem(scope_uri='doc_uri_here', section='section')
            ]
        )
    ).result()
    

Publish Diagnostics

textDocument/publishDiagnostics notifications are sent from the server to the client to highlight errors or potential issues. e.g. syntax errors or unused variables.

Usually this notification is sent after document is opened, or on document content change:

@json_server.feature(TEXT_DOCUMENT_DID_OPEN)
async def did_open(ls, params: DidOpenTextDocumentParams):
    """Text document did open notification."""
    ls.show_message("Text Document Did Open")
    ls.show_message_log("Validating json...")

    # Get document from workspace
    text_doc = ls.workspace.get_text_document(params.text_document.uri)

    diagnostic = Diagnostic(
       range=Range(
           start=Position(line-1, col-1),
           end=Position(line-1, col)
       ),
       message="Custom validation message",
       source="Json Server"
    )

    # Send diagnostics
    ls.publish_diagnostics(text_doc.uri, [diagnostic])

Show Message

window/showMessage is a notification that is sent from the server to the client to display a prominant text message. e.g. VSCode will render this as a notification popup

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING)
async def count_down_10_seconds_non_blocking(ls, *args):
    for i in range(10):
        # Sends message notification to the client
        ls.show_message(f"Counting down... {10 - i}")
        await asyncio.sleep(1)

Show Message Log

window/logMessage is a notification that is sent from the server to the client to display a discrete text message. e.g. VSCode will display the message in an Output channel.

@json_server.command(JsonLanguageServer.CMD_COUNT_DOWN_NON_BLOCKING)
async def count_down_10_seconds_non_blocking(ls, *args):
    for i in range(10):
        # Sends message log notification to the client
        ls.show_message_log(f"Counting down... {10 - i}")
        await asyncio.sleep(1)

Workspace Edits

The workspace/applyEdit request allows your language server to ask the client to modify particular documents in the client’s workspace.

def apply_edit(self, edit: WorkspaceEdit, label: str = None) -> ApplyWorkspaceEditResponse:
    # Omitted

def apply_edit_async(self, edit: WorkspaceEdit, label: str = None) -> ApplyWorkspaceEditResponse:
    # Omitted

Custom Notifications

Warning

Custom notifications are not part of the LSP specification and dedicated support for your custom notification(s) will have to be added to each language client you intend to support.

A custom notification can be sent to the client using the send_notification() method

server.send_notification('myCustomNotification', 'test data')

The Workspace

The Workspace is a python object that holds information about workspace folders, opened documents and is responsible for synchronising server side document state with that of the client.

Text Documents

The TextDocument class is how pygls represents a text document. Given a text document’s uri the get_text_document() method can be used to access the document itself:

@json_server.feature(TEXT_DOCUMENT_DID_OPEN)
async def did_open(ls, params: DidOpenTextDocumentParams):

    # Get document from workspace
    text_doc = ls.workspace.get_text_document(params.text_document.uri)

Notebook Documents

See also

See the section on notebookDocument/synchronization in the specification for full details on how notebook documents are handled

  • A notebook’s structure, metadata etc. is represented using the NotebookDocument class from lsprotocol.

  • The contents of a single notebook cell is represented using a standard TextDocument

In order to receive notebook documents from the client, your language server must provide an instance of NotebookDocumentSyncOptions which declares the kind of notebooks it is interested in

server = LanguageServer(
    name="example-server",
    version="v0.1",
    notebook_document_sync=types.NotebookDocumentSyncOptions(
        notebook_selector=[
            types.NotebookDocumentSyncOptionsNotebookSelectorType2(
                cells=[
                    types.NotebookDocumentSyncOptionsNotebookSelectorType2CellsType(
                        language="python"
                    )
                ]
            )
        ]
    ),
)

To access the contents of a notebook cell you would call the workspace’s get_text_document() method as normal.

cell_doc = ls.workspace.get_text_document(cell_uri)

To access the notebook itself call the workspace’s get_notebook_document() method with either the uri of the notebook or the uri of any of its cells.

notebook_doc = ls.workspace.get_notebook_document(notebook_uri=notebook_uri)

# -- OR --

notebook_doc = ls.workspace.get_notebook_document(cell_uri=cell_uri)