diff --git a/.gitignore b/.gitignore index 6abec623..a71328bb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ /stubs /doc/_build /.vscode -.php_cs.cache \ No newline at end of file +.php_cs.cache +.phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 93% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 5cbd7d7a..2c205486 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -8,7 +8,7 @@ ]) ; -return PhpCsFixer\Config::create() +return (new PhpCsFixer\Config()) ->setRiskyAllowed(true) ->setRules([ '@PSR2' => true, diff --git a/composer.json b/composer.json index 5db84215..079500f1 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "require-dev": { "amphp/phpunit-util": "^1.3", "ergebnis/composer-normalize": "^2.0", - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^0.8.0", "phpactor/phly-event-dispatcher": "~2.0.0", "phpactor/test-utils": "~1.1.3", diff --git a/lib/Core/Server/Client/WorkDoneProgressClient.php b/lib/Core/Server/Client/WorkDoneProgressClient.php new file mode 100644 index 00000000..5e6f0aff --- /dev/null +++ b/lib/Core/Server/Client/WorkDoneProgressClient.php @@ -0,0 +1,95 @@ +client = $client; + } + + /** + * @return Promise + */ + public function create(WorkDoneToken $token): Promise + { + return \Amp\call(function () use ($token) { + return yield $this->client->request('window/workDoneProgress/create', [ + 'token' => (string) $token, + ]); + }); + } + + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + self::assertIsValidPercentage($percentage); + + $this->notify($token, [ + 'kind' => 'begin', + 'title' => $title, + 'message' => $message, + 'percentage' => $percentage, + 'cancellable' => $cancellable, + ]); + } + + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + self::assertIsValidPercentage($percentage); + + $this->notify($token, [ + 'kind' => 'report', + 'message' => $message, + 'percentage' => $percentage, + 'cancellable' => $cancellable, + ]); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->notify($token, [ + 'kind' => 'end', + 'message' => $message, + ]); + } + + private static function assertIsValidPercentage(?int $percentage): void + { + if (!(null === $percentage || 0 <= $percentage && $percentage <= 100)) { + throw new InvalidArgumentException( + 'The percentage must be an integer comprised between 0 and 100.', + ); + } + } + + private function notify(WorkDoneToken $token, array $value): void + { + assert(in_array($value['kind'], ['begin', 'report', 'end'])); + + $this->client->notification('$/progress', [ + 'token' => (string) $token, + 'value' => $value, + ]); + } +} diff --git a/lib/Core/Server/ClientApi.php b/lib/Core/Server/ClientApi.php index ca008baa..ab29ec27 100644 --- a/lib/Core/Server/ClientApi.php +++ b/lib/Core/Server/ClientApi.php @@ -5,6 +5,7 @@ use Phpactor\LanguageServer\Core\Server\Client\ClientClient; use Phpactor\LanguageServer\Core\Server\Client\DiagnosticsClient; use Phpactor\LanguageServer\Core\Server\Client\WindowClient; +use Phpactor\LanguageServer\Core\Server\Client\WorkDoneProgressClient; use Phpactor\LanguageServer\Core\Server\Client\WorkspaceClient; final class ClientApi @@ -38,4 +39,9 @@ public function diagnostics(): DiagnosticsClient { return new DiagnosticsClient($this->client); } + + public function workDoneProgress(): WorkDoneProgressClient + { + return new WorkDoneProgressClient($this->client); + } } diff --git a/lib/Core/Server/RpcClient/TestRpcClient.php b/lib/Core/Server/RpcClient/TestRpcClient.php index ee530be3..40a71b86 100644 --- a/lib/Core/Server/RpcClient/TestRpcClient.php +++ b/lib/Core/Server/RpcClient/TestRpcClient.php @@ -3,7 +3,6 @@ namespace Phpactor\LanguageServer\Core\Server\RpcClient; use Amp\Promise; -use Phpactor\LanguageServer\Core\Server\ResponseWatcher; use Phpactor\LanguageServer\Core\Server\ResponseWatcher\TestResponseWatcher; use Phpactor\LanguageServer\Core\Server\RpcClient; use Phpactor\LanguageServer\Core\Server\Transmitter\TestMessageTransmitter; diff --git a/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php new file mode 100644 index 00000000..9a53ac2c --- /dev/null +++ b/lib/WorkDoneProgress/ClientCapabilityDependentProgressNotifier.php @@ -0,0 +1,62 @@ +window['workDoneProgress'] ?? false) { + $this->notifier = new WorkDoneProgressNotifier($api); + } else { + $this->notifier = new MessageProgressNotifier($api); + } + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return $this->notifier->create($token); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->notifier->begin($token, $title, $message, $percentage, $cancellable); + } + + /** + * {@inheritDoc} + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->notifier->report($token, $message, $percentage, $cancellable); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->notifier->end($token, $message); + } +} diff --git a/lib/WorkDoneProgress/MessageProgressNotifier.php b/lib/WorkDoneProgress/MessageProgressNotifier.php new file mode 100644 index 00000000..d7dccf44 --- /dev/null +++ b/lib/WorkDoneProgress/MessageProgressNotifier.php @@ -0,0 +1,60 @@ +api = $api->window()->showMessage(); + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return new Success(new ResponseMessage( + Uuid::uuid4(), + null, + )); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->info($message); + } + + /** + * {@inheritDoc} + */ + public function report(WorkDoneToken $token, ?string $message = null, ?int $percentage = null, ?bool $cancellable = null): void + { + $this->api->info(sprintf('%s - %d%%', $message, $percentage)); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->api->info($message); + } +} diff --git a/lib/WorkDoneProgress/ProgressNotifier.php b/lib/WorkDoneProgress/ProgressNotifier.php new file mode 100644 index 00000000..b69b0918 --- /dev/null +++ b/lib/WorkDoneProgress/ProgressNotifier.php @@ -0,0 +1,37 @@ + + */ + public function create(WorkDoneToken $token): Promise; + + /** + * @param int|null $percentage Percentage comprised between 0 and 100 + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void; + + /** + * @param int|null $percentage Percentage comprised between 0 and 100 + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void; + + public function end(WorkDoneToken $token, ?string $message = null): void; +} diff --git a/lib/WorkDoneProgress/WorkDoneProgressNotifier.php b/lib/WorkDoneProgress/WorkDoneProgressNotifier.php new file mode 100644 index 00000000..8a6973cf --- /dev/null +++ b/lib/WorkDoneProgress/WorkDoneProgressNotifier.php @@ -0,0 +1,58 @@ +api = $api->workDoneProgress(); + } + + /** + * {@inheritDoc} + */ + public function create(WorkDoneToken $token): Promise + { + return $this->api->create($token); + } + + /** + * {@inheritDoc} + */ + public function begin( + WorkDoneToken $token, + string $title, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->begin($token, $title, $message, $percentage, $cancellable); + } + + /** + * {@inheritDoc} + */ + public function report( + WorkDoneToken $token, + ?string $message = null, + ?int $percentage = null, + ?bool $cancellable = null + ): void { + $this->api->report($token, $message, $percentage, $cancellable); + } + + public function end(WorkDoneToken $token, ?string $message = null): void + { + $this->api->end($token, $message); + } +} diff --git a/lib/WorkDoneProgress/WorkDoneToken.php b/lib/WorkDoneProgress/WorkDoneToken.php new file mode 100644 index 00000000..1ac3599b --- /dev/null +++ b/lib/WorkDoneProgress/WorkDoneToken.php @@ -0,0 +1,28 @@ +token = $token; + } + + public function __toString(): string + { + return $this->token; + } + + public static function generate(): self + { + return new self((string) Uuid::uuid4()); + } +} diff --git a/tests/Unit/Core/Server/ClientApiTest.php b/tests/Unit/Core/Server/ClientApiTest.php index f13019e9..0a8d6cf1 100644 --- a/tests/Unit/Core/Server/ClientApiTest.php +++ b/tests/Unit/Core/Server/ClientApiTest.php @@ -15,6 +15,7 @@ use Phpactor\LanguageServerProtocol\WorkspaceEdit; use Phpactor\LanguageServer\Core\Server\ClientApi; use Phpactor\LanguageServer\Core\Server\RpcClient\TestRpcClient; +use Phpactor\LanguageServer\WorkDoneProgress\WorkDoneToken; class ClientApiTest extends AsyncTestCase { @@ -26,6 +27,7 @@ class ClientApiTest extends AsyncTestCase * @dataProvider provideWorkspaceExecuteCommand * @dataProvider provideDiagnostics * @dataProvider provideRegisterCapability + * @dataProvider provideWorkDoneProgress */ public function testSend(Closure $executor, Closure $assertions): void { @@ -37,7 +39,7 @@ public function testSend(Closure $executor, Closure $assertions): void } /** - * @reuturn Generator + * @return Generator */ public function provideWindowShowMessage(): Generator { @@ -70,7 +72,7 @@ function (TestRpcClient $client): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWindowLogMessage(): Generator { @@ -103,7 +105,7 @@ function (TestRpcClient $client): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWindowShowMessageRequest(): Generator { @@ -126,7 +128,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWorkspaceEdit(): Generator { @@ -151,7 +153,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideWorkspaceExecuteCommand(): Generator { @@ -171,7 +173,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideDiagnostics(): Generator { @@ -192,7 +194,7 @@ function (TestRpcClient $client, $result): void { } /** - * @reuturn Generator + * @return Generator */ public function provideRegisterCapability(): Generator { @@ -222,4 +224,69 @@ function (TestRpcClient $client, $result): void { } ]; } + + /** + * @return Generator + */ + public function provideWorkDoneProgress(): Generator + { + yield 'Creating Work Done Progress' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->create($token); + }, + function (TestRpcClient $client): void { + $message = $client->transmitter()->shiftRequest(); + self::assertEquals('window/workDoneProgress/create', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + } + ]; + + yield 'Work Done Progress Begin' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->begin($token, 'title', 'message'); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('begin', $message->params['value']['kind']); + self::assertEquals('title', $message->params['value']['title']); + self::assertEquals('message', $message->params['value']['message']); + self::assertNull($message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); + } + ]; + + yield 'Work Done Progress Report' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->report($token, 'message', 20); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('report', $message->params['value']['kind']); + self::assertEquals('message', $message->params['value']['message']); + self::assertEquals(20, $message->params['value']['percentage']); + self::assertNull($message->params['value']['cancellable']); + } + ]; + + yield 'Work Done Progress End' => [ + function (ClientApi $api): void { + $token = new WorkDoneToken('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15'); + $api->workDoneProgress()->end($token, 'message'); + }, + function (TestRpcClient $client, $result): void { + $message = $client->transmitter()->shiftNotification(); + self::assertEquals('$/progress', $message->method); + self::assertEquals('4ef439b3-3c6a-4c98-ae0a-af2b4503cb15', $message->params['token']); + self::assertEquals('end', $message->params['value']['kind']); + self::assertEquals('message', $message->params['value']['message']); + } + ]; + } } diff --git a/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php new file mode 100644 index 00000000..5ae9ac10 --- /dev/null +++ b/tests/Unit/WorkDoneProgress/ClientCapabilityDependentProgressNotifierTest.php @@ -0,0 +1,118 @@ +client = TestRpcClient::create(); + $this->transmitter = $this->client->transmitter(); + } + + public function testNotifyWithWorkDoneProgressCapability(): void + { + $token = WorkDoneToken::generate(); + $notifier = $this->createNotifierWithProgressCapability(); + + $notifier->create($token); + $message = $this->transmitter->shiftRequest(); + $this->assertEquals('window/workDoneProgress/create', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + + $notifier->begin($token, 'title', 'begin message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('begin', $message->params['value']['kind']); + $this->assertEquals('title', $message->params['value']['title']); + $this->assertEquals('begin message', $message->params['value']['message']); + $this->assertNull($message->params['value']['percentage']); + $this->assertNull($message->params['value']['cancellable']); + + $notifier->report($token, 'report message', 30); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('report', $message->params['value']['kind']); + $this->assertEquals('report message', $message->params['value']['message']); + $this->assertEquals(30, $message->params['value']['percentage']); + $this->assertNull($message->params['value']['cancellable']); + + $notifier->end($token, 'end message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('$/progress', $message->method); + $this->assertEquals((string) $token, $message->params['token']); + $this->assertEquals('end', $message->params['value']['kind']); + $this->assertEquals('end message', $message->params['value']['message']); + } + + public function testNotifyWithoutWorkDoneProgressCapability(): void + { + $token = WorkDoneToken::generate(); + $notifier = $this->createNotifierWithoutProgressCapability(); + + $notifier->create($token); + $message = $this->transmitter->shiftNotification(); + $this->assertNull($message); // Fake response so no message sent + + $notifier->begin($token, 'title', 'begin message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('begin message', $message->params['message']); + + $notifier->report($token, 'report message', 30); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('report message - 30%', $message->params['message']); + + $notifier->end($token, 'end message'); + $message = $this->transmitter->shiftNotification(); + $this->assertEquals('window/showMessage', $message->method); + $this->assertEquals(MessageType::INFO, $message->params['type']); + $this->assertEquals('end message', $message->params['message']); + } + + private function createNotifierWithProgressCapability() : ClientCapabilityDependentProgressNotifier + { + return $this->createNotifier(true); + } + + private function createNotifierWithoutProgressCapability() : ClientCapabilityDependentProgressNotifier + { + return $this->createNotifier(false); + } + + private function createNotifier(bool $WorkDoneProgress) : ClientCapabilityDependentProgressNotifier + { + $api = new ClientApi($this->client); + $capabilities = ClientCapabilities::fromArray([ + 'window' => ['workDoneProgress' => $WorkDoneProgress], + ]); + + return new ClientCapabilityDependentProgressNotifier($api, $capabilities); + } +}