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 theClientApi
.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 byPsr\EventDispatcher\ListenerProviderInterface
classes. We use:ServiceListener
: responsible to start all the services when the server isinitialized
.WorkspaceListener
: will update the above mentionedWorkspace
based on events emitted by theTextDocumentHandler
.
Handlers
: Method handlers are responsible for handling incoming method requests, this is the main extension point, see Method HandlersHandlerMethodRunner
: 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
: Handlesshutdown
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 ourResponseWatcher
.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.