Creating a Language Server

In the previous tutorial we used the ClosureDispatcherFactory. This is fine, but let’s now implement our own application - AcmeLS and give it a dedicated dispatcher factory AcmeLsDispatcherFactory. This will be the ingress for a new session:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env php
<?php

require __DIR__ . '/../../vendor/autoload.php';

use AcmeLs\AcmeLsDispatcherFactory;
use Phpactor\LanguageServer\LanguageServerBuilder;
use Psr\Log\NullLogger;

$logger = new NullLogger();
LanguageServerBuilder::create(new AcmeLsDispatcherFactory($logger))
    ->build()
    ->run();

The dispatcher is responsible for bootstrapping your language server session and creating all the necessary classes that you will need. You might, for example, instantiate a container here using some initialization options form the client.

The Language Server invokes the factory method of this class with two necessary dependencies: MessageTransmitter and the InitializeParams.

Let’s just jump in at the deep end:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<?php

namespace AcmeLs;

use Phpactor\LanguageServer\Adapter\Psr\AggregateEventDispatcher;
use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\PassThroughArgumentResolver;
use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\LanguageSeverProtocolParamsResolver;
use Phpactor\LanguageServer\Core\Dispatcher\ArgumentResolver\ChainArgumentResolver;
use Phpactor\LanguageServer\Core\Workspace\Workspace;
use Phpactor\LanguageServer\Listener\WorkspaceListener;
use Phpactor\LanguageServer\Middleware\CancellationMiddleware;
use Phpactor\LanguageServer\Middleware\ErrorHandlingMiddleware;
use Phpactor\LanguageServer\Middleware\InitializeMiddleware;
use Phpactor\LanguageServerProtocol\InitializeParams;
use Phpactor\LanguageServer\Core\Dispatcher\Dispatcher;
use Phpactor\LanguageServer\Core\Handler\HandlerMethodRunner;
use Phpactor\LanguageServer\Core\Dispatcher\DispatcherFactory;
use Phpactor\LanguageServer\Handler\System\ExitHandler;
use Phpactor\LanguageServer\Handler\Workspace\CommandHandler;
use Phpactor\LanguageServer\Middleware\ResponseHandlingMiddleware;
use Phpactor\LanguageServer\Core\Command\CommandDispatcher;
use Phpactor\LanguageServer\Handler\System\ServiceHandler;
use Phpactor\LanguageServer\Core\Handler\Handlers;
use Phpactor\LanguageServer\Handler\TextDocument\TextDocumentHandler;
use Phpactor\LanguageServer\Core\Dispatcher\Dispatcher\MiddlewareDispatcher;
use Phpactor\LanguageServer\Listener\ServiceListener;
use Phpactor\LanguageServer\Core\Server\RpcClient\JsonRpcClient;
use Phpactor\LanguageServer\Core\Service\ServiceManager;
use Phpactor\LanguageServer\Core\Service\ServiceProviders;
use Phpactor\LanguageServer\Example\Service\PingProvider;
use Phpactor\LanguageServer\Core\Server\ClientApi;
use Phpactor\LanguageServer\Core\Server\ResponseWatcher\DeferredResponseWatcher;
use Phpactor\LanguageServer\Core\Server\Transmitter\MessageTransmitter;
use Phpactor\LanguageServer\Middleware\HandlerMiddleware;
use Phpactor\LanguageServer\Middleware\ShutdownMiddleware;
use Psr\Log\LoggerInterface;

class AcmeLsDispatcherFactory implements DispatcherFactory
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function create(MessageTransmitter $transmitter, InitializeParams $initializeParams): Dispatcher
    {
        $responseWatcher = new DeferredResponseWatcher();
        $clientApi = new ClientApi(new JsonRpcClient($transmitter, $responseWatcher));

        $serviceProviders = new ServiceProviders(
            new PingProvider($clientApi)
        );

        $serviceManager = new ServiceManager($serviceProviders, $this->logger);
        $workspace = new Workspace();

        $eventDispatcher = new AggregateEventDispatcher(
            new ServiceListener($serviceManager),
            new WorkspaceListener($workspace)
        );

        $handlers = new Handlers(
            new TextDocumentHandler($eventDispatcher),
            new ServiceHandler($serviceManager, $clientApi),
            new CommandHandler(new CommandDispatcher([])),
        );

        $runner = new HandlerMethodRunner(
            $handlers,
            new ChainArgumentResolver(
                new LanguageSeverProtocolParamsResolver(),
                new PassThroughArgumentResolver()
            ),
        );

        return new MiddlewareDispatcher(
            new ErrorHandlingMiddleware($this->logger),
            new InitializeMiddleware($handlers, $eventDispatcher, [
                'name' => 'acme',
                'version' => '1',
            ]),
            new ShutdownMiddleware($eventDispatcher),
            new ResponseHandlingMiddleware($responseWatcher),
            new CancellationMiddleware($runner),
            new HandlerMiddleware($runner)
        );
    }
}
  • MessageTransmitter: This class is provided by the Language Server and allows you to send messages to the connected client. This is quite low-level, instead you should use the ClientApi.
  • InitializeParams: The initialization parameters provided by the client.
  • ResponseWatcher: Class which tracks requests made by the server to the client and can resolve responses, used as a dependency for…
  • ClientApi: This class allows you to send (and receive) messages to the client. It provides a convenient API $clientApi->window()->showMessage()->error('Foobar'). In cases where the API doesn’t provide what you need you can use the …
  • RpcClient which allows you to send requests and notifications to the client.
  • ServiceProviders, PingProvider, ServiceManager: Ping provider is an annoying service which pings your client for no reason at all, it is an example background process. See Service Providers for more information on services.
  • Workspace: This class can keeps track of LSP text documents.
  • EventDispatcher: Required by some middlewares to transmit events which can be handled by Psr\EventDispatcher\ListenerProviderInterface classes. We use:
    • ServiceListener: responsible to start all the services when the server is initialized.
    • WorkspaceListener: will update the above mentioned Workspace based on events emitted by the TextDocumentHandler.
  • Handlers: Method handlers are responsible for handling incoming method requests, this is the main extension point, see Method Handlers
  • HandlerMethodRunner: This class is responsible for calling methods on your class and converting the array of parameters from the request to match the parameters on a handler’s method. Find out more in Method Handlers.

The RPC method handlers:

  • TextDocumentHandler: Handles all text document notifications from the client (i.e. text document synchronization). It emmits events.
  • ServiceHandler: Non-protocol handler for starting/stopping monitoring services.
  • CommandHandler: Clients can execute commands (e.g. refactor something) on the server, this class handlers that. See Commands.
  • ExitHandler: Handles shutdown notifications from the client.

Finally we build the middleware dispatcher with the middlewares which will handle the request:

  • ErrorHandlingMiddleware: Will catch any errors thrown by succeeding middlewares and log them. As a long running process we don’t want to exit each time something goes wrong.
  • InitializeMiddleware: This middleware responds to the initialize request. It also allows your handlers to inject capabiltities into the response, more in Method Handlers.
  • ResponseHandlingMiddleware: Catch responses to requests made by the server, and resolves them using our ResponseWatcher.
  • CancellationMiddleware: Often the client knows that a request is no longer required, and it request that that request be cancelled (imagine a long-running search). This middleware intercepts the $/cancelRequest notifications and tells the runner to cancel them.
  • HandlerMiddleware: The final destination - will forward the request to the handler runner which will dispatch our handlers

In your application you might choose to connect all of this magic in a dependency injection container.