How To Extend the Default LanguageClient

There will likely come a point where you will want to modify some aspect of the default language client’s behaviour - or replace it entirely with your own. This guide will walk through the various options for adjusting the client and its behaviour

Adding new methods

If you need to add support to the client for an LSP method it does not yet support, this can be done inside your setup fixture.

@pytest_lsp.fixture(
    config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
)
async def client(lsp_client: LanguageClient):
    # Register a custom `workspace/diagnostic/refresh` implementation
    lsp_client.refresh_requests = 0

    @lsp_client.feature(types.WORKSPACE_DIAGNOSTIC_REFRESH)
    def refresh(cl: LanguageClient, params: None):
        cl.refresh_requests += 1

    # Setup
    await lsp_client.initialize_session(
        types.InitializeParams(capabilities=types.ClientCapabilities())
    )

    yield

    # Teardown
    await lsp_client.shutdown_session()

Replacing methods

Replacing an existing method’s implementation will require you to setup your own client factory function.

  1. First write your custom method implementation. As an example, we’ll fail the test if the server tries to send diagnostics via the textDocument/publishDiagnostics notification.

    def disallow_publish_diagnostics(
        client: LanguageClient, params: types.PublishDiagnosticsParams
    ):
        """Raises an error if the server calls ``textDocument/publishDiagnostics``"""
        raise RuntimeError("The server should not use `textDocument/publishDiagnostics`")
    
  2. Write your own client factory function, you will need to construct your own mapping from lsp method names to the corresponding handler functions.

    The DEFAULT_CLIENT_FEATURES dictionary will include all of the built in handlers.

    Pass your mapping and client instance to the register_lsp_features() function to register them.

    from pygls.protocol import default_converter
    
    from pytest_lsp.client import DEFAULT_CLIENT_FEATURES, register_lsp_features
    
    
    def my_make_test_lsp_client() -> LanguageClient:
        """Return a customised ``LanguageClient`` instance"""
        client = LanguageClient(
            converter_factory=default_converter,
        )
    
        features = {
            **DEFAULT_CLIENT_FEATURES,
            types.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS: disallow_publish_diagnostics,
        }
    
        register_lsp_features(client, features)
        return client
    
  3. Finally, use your custom client factory function with the ClientServerConfig you pass to your fixture function.

    @pytest_lsp.fixture(
        config=ClientServerConfig(
            client_factory=my_make_test_lsp_client,
            server_command=[sys.executable, "server.py"],
        ),
    )
    async def client(lsp_client: LanguageClient):
        # Setup
        await lsp_client.initialize_session(
            types.InitializeParams(capabilities=types.ClientCapabilities())
        )
    
        yield
    
        # Teardown
        await lsp_client.shutdown_session()
    

Using a Custom Client Class

Using your own custom LanguageClient class is very similar to Replacing methods, just create an instance of your language client in your factory function.

Important

Your custom language client must inherit from the default LanguageCient class

from pygls.protocol import default_converter
from pytest_lsp import LanguageClient
from pytest_lsp.client import DEFAULT_CLIENT_FEATURES, register_lsp_features

class MyLanguageClient(LanguageClient):
    pass

def my_make_test_lsp_client() -> LanguageClient:
    """Return a custom language client"""
    client = MyLanguageClient(
        converter_factory=default_converter,
    )
    register_lsp_features(client, DEFAULT_CLIENT_FEATURES)
    return client