diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d15f9af..93b9bcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +1.0.2 +----- + +- Add support for file events + 0.2.0 ----- diff --git a/bin/serve.php b/bin/serve.php index a9009d49..0aa1a583 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -23,6 +23,8 @@ use Phpactor\LanguageServer\Example\Command\SayHelloCommand; use Phpactor\LanguageServer\Example\Diagnostics\SayHelloDiagnosticsProvider; use Phpactor\LanguageServer\Handler\TextDocument\CodeActionHandler; +use Phpactor\LanguageServer\Handler\Workspace\DidChangeWatchedFilesHandler; +use Phpactor\LanguageServer\Listener\DidChangeWatchedFilesListener; use Phpactor\LanguageServer\Listener\ServiceListener; use Phpactor\LanguageServer\Core\Service\ServiceManager; use Phpactor\LanguageServer\Core\Service\ServiceProviders; @@ -38,6 +40,7 @@ use Phpactor\LanguageServer\Middleware\HandlerMiddleware; use Phpactor\LanguageServer\Middleware\InitializeMiddleware; use Phpactor\LanguageServer\Core\Command\CommandDispatcher; +use Phpactor\LanguageServer\Middleware\ResponseHandlingMiddleware; use Phpactor\LanguageServer\Service\DiagnosticsService; use Psr\Log\AbstractLogger; use function Safe\fopen; @@ -107,6 +110,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge $eventDispatcher = new AggregateEventDispatcher( new ServiceListener($serviceManager), new WorkspaceListener($workspace), + new DidChangeWatchedFilesListener($clientApi, ['**/*.php']), $diagnosticsService ); @@ -117,6 +121,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge new CommandHandler(new CommandDispatcher([ 'phpactor.say_hello' => new SayHelloCommand($clientApi) ])), + new DidChangeWatchedFilesHandler($eventDispatcher), new CodeActionHandler(new AggregateCodeActionProvider( new SayHelloCodeActionProvider() ), $workspace), @@ -137,6 +142,7 @@ function (MessageTransmitter $transmitter, InitializeParams $params) use ($logge 'version' => 1, ]), new CancellationMiddleware($runner), + new ResponseHandlingMiddleware($responseWatcher), new HandlerMiddleware($runner) ); } diff --git a/composer.json b/composer.json index 349cdcee..8ee31b54 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { diff --git a/lib/Core/Server/Client/ClientClient.php b/lib/Core/Server/Client/ClientClient.php index 959652d5..392e6214 100644 --- a/lib/Core/Server/Client/ClientClient.php +++ b/lib/Core/Server/Client/ClientClient.php @@ -2,8 +2,10 @@ namespace Phpactor\LanguageServer\Core\Server\Client; +use Amp\Promise; use Phpactor\LanguageServerProtocol\Registration; use Phpactor\LanguageServerProtocol\Unregistration; +use Phpactor\LanguageServer\Core\Rpc\ResponseMessage; use Phpactor\LanguageServer\Core\Server\RpcClient; final class ClientClient @@ -18,16 +20,22 @@ public function __construct(RpcClient $client) $this->client = $client; } - public function registerCapability(Registration ...$registrations): void + /** + * @return Promise + */ + public function registerCapability(Registration ...$registrations): Promise { - $this->client->notification('client/registerCapability', [ + return $this->client->request('client/registerCapability', [ 'registrations' => $registrations ]); } - public function unregisterCapability(Unregistration ...$unregistrations): void + /** + * @return Promise + */ + public function unregisterCapability(Unregistration ...$unregistrations): Promise { - $this->client->notification('client/unregisterCapability', [ + return $this->client->request('client/unregisterCapability', [ 'unregistrations' => $unregistrations ]); } diff --git a/lib/Event/FilesChanged.php b/lib/Event/FilesChanged.php new file mode 100644 index 00000000..8498190f --- /dev/null +++ b/lib/Event/FilesChanged.php @@ -0,0 +1,23 @@ +events = $events; + } + + public function events(): array + { + return $this->events; + } +} diff --git a/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php b/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php new file mode 100644 index 00000000..cc8995c5 --- /dev/null +++ b/lib/Handler/Workspace/DidChangeWatchedFilesHandler.php @@ -0,0 +1,36 @@ +dispatcher = $dispatcher; + } + + /** + * {@inheritDoc} + */ + public function methods(): array + { + return [ + 'workspace/didChangeWatchedFiles' => 'didChange' + ]; + } + + public function didChange(DidChangeWatchedFilesParams $params): void + { + $this->dispatcher->dispatch(new FilesChanged(...$params->changes)); + } +} diff --git a/lib/LanguageServerTesterBuilder.php b/lib/LanguageServerTesterBuilder.php index 2f6ea815..97f42401 100644 --- a/lib/LanguageServerTesterBuilder.php +++ b/lib/LanguageServerTesterBuilder.php @@ -25,6 +25,8 @@ use Phpactor\LanguageServer\Handler\System\ServiceHandler; use Phpactor\LanguageServer\Handler\TextDocument\TextDocumentHandler; use Phpactor\LanguageServer\Handler\Workspace\CommandHandler; +use Phpactor\LanguageServer\Handler\Workspace\DidChangeWatchedFilesHandler; +use Phpactor\LanguageServer\Listener\DidChangeWatchedFilesListener; use Phpactor\LanguageServer\Listener\ServiceListener; use Phpactor\LanguageServer\Listener\WorkspaceListener; use Phpactor\LanguageServer\Middleware\HandlerMiddleware; @@ -108,6 +110,11 @@ final class LanguageServerTesterBuilder */ private $enableServices = false; + /** + * @var bool + */ + private $enableFileEvents = false; + /** * @var bool */ @@ -128,6 +135,11 @@ final class LanguageServerTesterBuilder */ private $diagnosticsProvider = []; + /** + * @var string[] + */ + private $fileEventGlobs = ['**/*.php']; + private function __construct() { $this->initializeParams = new InitializeParams(new ClientCapabilities()); @@ -224,6 +236,21 @@ public function enableTextDocuments(): self return $this; } + /** + * Enable file events + * @param string[] $globs + */ + public function enableFileEvents(?array $globs = null): self + { + $this->enableFileEvents = true; + + if (null !==$globs) { + $this->fileEventGlobs = $globs; + } + + return $this; + } + /** * Enable the services (enabled by default with ::create) */ @@ -313,6 +340,10 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $this->listeners[] = $service; } + if ($this->enableFileEvents) { + $this->listeners[] = new DidChangeWatchedFilesListener($this->clientApi, $this->fileEventGlobs); + } + $serviceManager = new ServiceManager(new ServiceProviders(...$serviceProviders), $logger); $eventDispatcher = $this->buildEventDispatcher($serviceManager); @@ -326,6 +357,10 @@ function (MessageTransmitter $transmitter, InitializeParams $params) { $handlers[] = new ServiceHandler($serviceManager, $this->clientApi); } + if ($this->enableFileEvents) { + $handlers[] = new DidChangeWatchedFilesHandler($eventDispatcher); + } + if ($this->enableCommands) { $handlers[] = new CommandHandler(new CommandDispatcher($this->commands)); } diff --git a/lib/Listener/DidChangeWatchedFilesListener.php b/lib/Listener/DidChangeWatchedFilesListener.php new file mode 100644 index 00000000..2922dd92 --- /dev/null +++ b/lib/Listener/DidChangeWatchedFilesListener.php @@ -0,0 +1,58 @@ +client = $client; + $this->globPatterns = $globPatterns; + } + + /** + * {@inheritDoc} + */ + public function getListenersForEvent(object $event): iterable + { + if ($event instanceof Initialized) { + return [[$this, 'registerCapability']]; + } + + return []; + } + + public function registerCapability(Initialized $initialized): void + { + asyncCall(function () { + yield $this->client->client()->registerCapability( + new Registration( + Uuid::uuid4()->__toString(), + 'workspace/didChangeWatchedFiles', + new DidChangeWatchedFilesRegistrationOptions(array_map(function (string $glob) { + return new FileSystemWatcher($glob); + }, $this->globPatterns)) + ) + ); + }); + } +} diff --git a/lib/Test/ListenerProvider/RecordingListenerProvider.php b/lib/Test/ListenerProvider/RecordingListenerProvider.php new file mode 100644 index 00000000..b12e656e --- /dev/null +++ b/lib/Test/ListenerProvider/RecordingListenerProvider.php @@ -0,0 +1,48 @@ +recieved[] = $event; + } + ]; + } + + /** + * @param string $type + */ + public function shift(string $type): object + { + $next = array_shift($this->recieved); + + if (null === $next) { + throw new RuntimeException('No more events'); + } + + if (!$next instanceof $type) { + throw new RuntimeException(sprintf( + 'Expected event of type "%s" but got "%s"', + $type, + get_class($next) + )); + } + + return $next; + } +} diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index f676fd66..f13019e9 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -205,7 +205,7 @@ function (ClientApi $api): void { ); }, function (TestRpcClient $client, $result): void { - $message = $client->transmitter()->shiftNotification(); + $message = $client->transmitter()->shiftRequest(); self::assertEquals('client/registerCapability', $message->method); } ]; @@ -217,7 +217,7 @@ function (ClientApi $api): void { ); }, function (TestRpcClient $client, $result): void { - $message = $client->transmitter()->shiftNotification(); + $message = $client->transmitter()->shiftRequest(); self::assertEquals('client/unregisterCapability', $message->method); } ]; diff --git a/tests/Unit/Handler/HandlerTestCase.php b/tests/Unit/Handler/HandlerTestCase.php index fb5a4a6f..30189d67 100644 --- a/tests/Unit/Handler/HandlerTestCase.php +++ b/tests/Unit/Handler/HandlerTestCase.php @@ -16,4 +16,11 @@ public function dispatch(string $method, array $params) return $tester->requestAndWait($method, $params); } + + public function notify(string $method, array $params) + { + $tester = LanguageServerTesterBuilder::createBare()->addHandler($this->handler())->build(); + + return $tester->notifyAndWait($method, $params); + } } diff --git a/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php b/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php new file mode 100644 index 00000000..6bb3500e --- /dev/null +++ b/tests/Unit/Handler/Workspace/DidChangeWatchedFilesHandlerTest.php @@ -0,0 +1,40 @@ +enableFileEvents() + ->build(); + $tester->initialize(); + + // capability registration happens after the intiialization request has been returned + $this->addToAssertionCount(1); + } + + public function testEmitsFileChangedEvents(): void + { + $events = new RecordingListenerProvider(); + $tester = LanguageServerTesterBuilder::create() + ->enableFileEvents() + ->addListenerProvider($events) + ->build(); + + $tester->notifyAndWait('workspace/didChangeWatchedFiles', new DidChangeWatchedFilesParams([ + new FileEvent('file://foobar', FileChangeType::CREATED) + ])); + $event = $events->shift(FilesChanged::class); + self::assertEquals(new FilesChanged(new FileEvent('file://foobar', FileChangeType::CREATED)), $event); + } +}