From 9283ba2148f70e1036674b8256873ef2293a34d2 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 29 Oct 2016 16:53:20 +0200 Subject: [PATCH] Handle Client responses --- src/Client/TextDocument.php | 26 +++--- src/Client/Window.php | 37 ++++----- src/ClientHandler.php | 81 +++++++++++++++++++ src/IdGenerator.php | 25 ++++++ src/LanguageClient.php | 14 ++-- src/LanguageServer.php | 10 ++- src/ProtocolReader.php | 10 ++- src/ProtocolStreamReader.php | 21 +---- src/ProtocolStreamWriter.php | 7 +- src/ProtocolWriter.php | 9 ++- tests/LanguageServerTest.php | 4 +- tests/MockProtocolStream.php | 22 ++--- tests/NodeVisitor/DefinitionCollectorTest.php | 4 +- tests/PhpDocumentTest.php | 2 +- tests/ProjectTest.php | 2 +- tests/ProtocolReadStreamTest.php | 2 +- tests/Server/ServerTestCase.php | 2 +- .../Definition/GlobalFallbackTest.php | 2 +- tests/Server/TextDocument/DidChangeTest.php | 2 +- tests/Server/TextDocument/DidCloseTest.php | 2 +- tests/Server/TextDocument/FormattingTest.php | 4 +- tests/Server/TextDocument/ParseErrorsTest.php | 10 ++- .../References/GlobalFallbackTest.php | 2 +- 23 files changed, 190 insertions(+), 110 deletions(-) create mode 100644 src/ClientHandler.php create mode 100644 src/IdGenerator.php diff --git a/src/Client/TextDocument.php b/src/Client/TextDocument.php index a67748f..d5314e4 100644 --- a/src/Client/TextDocument.php +++ b/src/Client/TextDocument.php @@ -3,9 +3,9 @@ declare(strict_types = 1); namespace LanguageServer\Client; -use AdvancedJsonRpc\Notification as NotificationBody; -use LanguageServer\ProtocolWriter; +use LanguageServer\ClientHandler; use LanguageServer\Protocol\Message; +use Sabre\Event\Promise; /** * Provides method handlers for all textDocument/* methods @@ -13,13 +13,13 @@ use LanguageServer\Protocol\Message; class TextDocument { /** - * @var ProtocolWriter + * @var ClientHandler */ - private $protocolWriter; + private $handler; - public function __construct(ProtocolWriter $protocolWriter) + public function __construct(ClientHandler $handler) { - $this->protocolWriter = $protocolWriter; + $this->handler = $handler; } /** @@ -27,15 +27,13 @@ class TextDocument * * @param string $uri * @param Diagnostic[] $diagnostics + * @return Promise */ - public function publishDiagnostics(string $uri, array $diagnostics) + public function publishDiagnostics(string $uri, array $diagnostics): Promise { - $this->protocolWriter->write(new Message(new NotificationBody( - 'textDocument/publishDiagnostics', - (object)[ - 'uri' => $uri, - 'diagnostics' => $diagnostics - ] - ))); + return $this->handler->notify('textDocument/publishDiagnostics', [ + 'uri' => $uri, + 'diagnostics' => $diagnostics + ]); } } diff --git a/src/Client/Window.php b/src/Client/Window.php index da925aa..053f306 100644 --- a/src/Client/Window.php +++ b/src/Client/Window.php @@ -3,9 +3,9 @@ declare(strict_types = 1); namespace LanguageServer\Client; -use AdvancedJsonRpc\Notification as NotificationBody; -use LanguageServer\ProtocolWriter; +use LanguageServer\ClientHandler; use LanguageServer\Protocol\Message; +use Sabre\Event\Promise; /** * Provides method handlers for all window/* methods @@ -13,30 +13,26 @@ use LanguageServer\Protocol\Message; class Window { /** - * @var ProtocolWriter + * @var ClientHandler */ - private $protocolWriter; + private $handler; - public function __construct(ProtocolWriter $protocolWriter) + public function __construct(ClientHandler $handler) { - $this->protocolWriter = $protocolWriter; + $this->handler = $handler; } /** - * The show message notification is sent from a server to a client to ask the client to display a particular message in the user interface. + * The show message notification is sent from a server to a client + * to ask the client to display a particular message in the user interface. * * @param int $type * @param string $message + * @return Promise */ - public function showMessage(int $type, string $message) + public function showMessage(int $type, string $message): Promise { - $this->protocolWriter->write(new Message(new NotificationBody( - 'window/showMessage', - (object)[ - 'type' => $type, - 'message' => $message - ] - ))); + return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]); } /** @@ -44,15 +40,10 @@ class Window * * @param int $type * @param string $message + * @return Promise */ - public function logMessage(int $type, string $message) + public function logMessage(int $type, string $message): Promise { - $this->protocolWriter->write(new Message(new NotificationBody( - 'window/logMessage', - (object)[ - 'type' => $type, - 'message' => $message - ] - ))); + return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]); } } diff --git a/src/ClientHandler.php b/src/ClientHandler.php new file mode 100644 index 0000000..c0e2740 --- /dev/null +++ b/src/ClientHandler.php @@ -0,0 +1,81 @@ +protocolReader = $protocolReader; + $this->protocolWriter = $protocolWriter; + $this->idGenerator = new IdGenerator; + } + + /** + * Sends a request to the client and returns a promise that is resolved with the result or rejected with the error + * + * @param string $method The method to call + * @param array|object $params The method parameters + * @return Promise Resolved with the result of the request or rejected with an error + */ + public function request(string $method, $params): Promise + { + $id = $this->idGenerator->generate(); + return $this->protocolWriter->write( + new Protocol\Message( + new AdvancedJsonRpc\Request($id, $method, (object)$params) + ) + )->then(function () use ($id) { + return new Promise(function ($resolve, $reject) use ($id) { + $listener = function (Protocol\Message $msg) use ($id, $listener) { + if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) { + // Received a response + $this->protocolReader->removeListener($listener); + if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) { + $resolve($msg->body->result); + } else { + $reject($msg->body->error); + } + } + }; + $this->protocolReader->on('message', $listener); + }); + }); + } + + /** + * Sends a notification to the client + * + * @param string $method The method to call + * @param array|object $params The method parameters + * @return Promise Will be resolved as soon as the notification has been sent + */ + public function notify(string $method, $params): Promise + { + $id = $this->idGenerator->generate(); + return $this->protocolWriter->write( + new Protocol\Message( + new AdvancedJsonRpc\Notification($method, (object)$params) + ) + ); + } +} diff --git a/src/IdGenerator.php b/src/IdGenerator.php new file mode 100644 index 0000000..d15ce8e --- /dev/null +++ b/src/IdGenerator.php @@ -0,0 +1,25 @@ +counter++; + } +} diff --git a/src/LanguageClient.php b/src/LanguageClient.php index de09c41..1f3c42a 100644 --- a/src/LanguageClient.php +++ b/src/LanguageClient.php @@ -3,9 +3,6 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Client\TextDocument; -use LanguageServer\Client\Window; - class LanguageClient { /** @@ -21,13 +18,12 @@ class LanguageClient * @var Client\Window */ public $window; - - private $protocolWriter; - public function __construct(ProtocolWriter $writer) + public function __construct(ProtocolReader $reader, ProtocolWriter $writer) { - $this->protocolWriter = $writer; - $this->textDocument = new TextDocument($writer); - $this->window = new Window($writer); + $handler = new ClientHandler($reader, $writer); + + $this->textDocument = new Client\TextDocument($handler); + $this->window = new Client\Window($handler); } } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 13325fc..11a45db 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -3,7 +3,6 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Server\TextDocument; use LanguageServer\Protocol\{ ServerCapabilities, ClientCapabilities, @@ -15,7 +14,6 @@ use LanguageServer\Protocol\{ }; use AdvancedJsonRpc; use Sabre\Event\Loop; -use JsonMapper; use Exception; use Throwable; @@ -56,7 +54,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher { parent::__construct($this, '/'); $this->protocolReader = $reader; - $this->protocolReader->onMessage(function (Message $msg) { + $this->protocolReader->on('message', function (Message $msg) { + // Ignore responses, this is the handler for requests and notifications + if (AdvancedJsonRpc\Response::isResponse($msg->body)) { + return; + } $result = null; $error = null; try { @@ -81,7 +83,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } }); $this->protocolWriter = $writer; - $this->client = new LanguageClient($writer); + $this->client = new LanguageClient($reader, $writer); $this->project = new Project($this->client); diff --git a/src/ProtocolReader.php b/src/ProtocolReader.php index 1370504..7515bab 100644 --- a/src/ProtocolReader.php +++ b/src/ProtocolReader.php @@ -3,7 +3,13 @@ declare(strict_types = 1); namespace LanguageServer; -interface ProtocolReader +use Sabre\Event\EmitterInterface; + +/** + * Must emit a "message" event with a Protocol\Message object as parameter + * when a message comes in + */ +interface ProtocolReader extends EmitterInterface { - public function onMessage(callable $listener); + } diff --git a/src/ProtocolStreamReader.php b/src/ProtocolStreamReader.php index 2d0e351..af4f3ed 100644 --- a/src/ProtocolStreamReader.php +++ b/src/ProtocolStreamReader.php @@ -5,9 +5,9 @@ namespace LanguageServer; use LanguageServer\Protocol\Message; use AdvancedJsonRpc\Message as MessageBody; -use Sabre\Event\Loop; +use Sabre\Event\{Loop, Emitter}; -class ProtocolStreamReader implements ProtocolReader +class ProtocolStreamReader extends Emitter implements ProtocolReader { const PARSE_HEADERS = 1; const PARSE_BODY = 2; @@ -17,7 +17,6 @@ class ProtocolStreamReader implements ProtocolReader private $buffer = ''; private $headers = []; private $contentLength; - private $listener; /** * @param resource $input @@ -43,11 +42,8 @@ class ProtocolStreamReader implements ProtocolReader break; case self::PARSE_BODY: if (strlen($this->buffer) === $this->contentLength) { - if (isset($this->listener)) { - $msg = new Message(MessageBody::parse($this->buffer), $this->headers); - $listener = $this->listener; - $listener($msg); - } + $msg = new Message(MessageBody::parse($this->buffer), $this->headers); + $this->emit('message', [$msg]); $this->parsingMode = self::PARSE_HEADERS; $this->headers = []; $this->buffer = ''; @@ -57,13 +53,4 @@ class ProtocolStreamReader implements ProtocolReader } }); } - - /** - * @param callable $listener Is called with a Message object - * @return void - */ - public function onMessage(callable $listener) - { - $this->listener = $listener; - } } diff --git a/src/ProtocolStreamWriter.php b/src/ProtocolStreamWriter.php index 41d7afc..3f51e14 100644 --- a/src/ProtocolStreamWriter.php +++ b/src/ProtocolStreamWriter.php @@ -31,12 +31,9 @@ class ProtocolStreamWriter implements ProtocolWriter } /** - * Sends a Message to the client - * - * @param Message $msg - * @return Promise Resolved when the message has been fully written out to the output stream + * {@inheritdoc} */ - public function write(Message $msg) + public function write(Message $msg): Promise { // if the message queue is currently empty, register a write handler. if (empty($this->messages)) { diff --git a/src/ProtocolWriter.php b/src/ProtocolWriter.php index 4055ddb..5ac237c 100644 --- a/src/ProtocolWriter.php +++ b/src/ProtocolWriter.php @@ -4,8 +4,15 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\Message; +use Sabre\Event\Promise; interface ProtocolWriter { - public function write(Message $msg); + /** + * Sends a Message to the client + * + * @param Message $msg + * @return Promise Resolved when the message has been fully written out to the output stream + */ + public function write(Message $msg): Promise; } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 18b580e..1c4e278 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -16,7 +16,7 @@ class LanguageServerTest extends TestCase $writer = new MockProtocolStream(); $server = new LanguageServer($reader, $writer); $msg = null; - $writer->onMessage(function (Message $message) use (&$msg) { + $writer->on('message', function (Message $message) use (&$msg) { $msg = $message; }); $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ @@ -24,7 +24,7 @@ class LanguageServerTest extends TestCase 'processId' => getmypid(), 'capabilities' => new ClientCapabilities() ]))); - $this->assertNotNull($msg, 'onMessage callback should be called'); + $this->assertNotNull($msg, 'message event should be emitted'); $this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); $this->assertEquals((object)[ 'capabilities' => (object)[ diff --git a/tests/MockProtocolStream.php b/tests/MockProtocolStream.php index a6cf1f4..5550b3f 100644 --- a/tests/MockProtocolStream.php +++ b/tests/MockProtocolStream.php @@ -5,34 +5,22 @@ namespace LanguageServer\Tests; use LanguageServer\{ProtocolReader, ProtocolWriter}; use LanguageServer\Protocol\Message; +use Sabre\Event\{Emitter, Promise}; /** * A fake duplex protocol stream */ -class MockProtocolStream implements ProtocolReader, ProtocolWriter +class MockProtocolStream extends Emitter implements ProtocolReader, ProtocolWriter { - private $listener; - /** * Sends a Message to the client * * @param Message $msg * @return void */ - public function write(Message $msg) + public function write(Message $msg): Promise { - if (isset($this->listener)) { - $listener = $this->listener; - $listener(Message::parse((string)$msg)); - } - } - - /** - * @param callable $listener Is called with a Message object - * @return void - */ - public function onMessage(callable $listener) - { - $this->listener = $listener; + $this->emit('message', [Message::parse((string)$msg)]); + return Promise\resolve(null); } } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 1ea7b68..0a8fdb1 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -15,7 +15,7 @@ class DefinitionCollectorTest extends TestCase { public function testCollectsSymbols() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); @@ -54,7 +54,7 @@ class DefinitionCollectorTest extends TestCase public function testDoesNotCollectReferences() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index c14a6b3..fce5dc4 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -19,7 +19,7 @@ class PhpDocumentTest extends TestCase public function setUp() { - $this->project = new Project(new LanguageClient(new MockProtocolStream())); + $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream)); } public function testParsesVariableVariables() diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index affbe2c..dc030d2 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -19,7 +19,7 @@ class ProjectTest extends TestCase public function setUp() { - $this->project = new Project(new LanguageClient(new MockProtocolStream())); + $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream)); } public function testGetDocumentLoadsDocument() diff --git a/tests/ProtocolReadStreamTest.php b/tests/ProtocolReadStreamTest.php index 5de0a7c..ca9c9f0 100644 --- a/tests/ProtocolReadStreamTest.php +++ b/tests/ProtocolReadStreamTest.php @@ -18,7 +18,7 @@ class ProtocolStreamReaderTest extends TestCase $writeHandle = fopen($tmpfile, 'w'); $reader = new ProtocolStreamReader(fopen($tmpfile, 'r')); $msg = null; - $reader->onMessage(function (Message $message) use (&$msg) { + $reader->on('message', function (Message $message) use (&$msg) { $msg = $message; }); $ret = fwrite($writeHandle, (string)new Message(new RequestBody(1, 'aMethod', ['arg' => 'Hello World']))); diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index cb4ed19..24095a3 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -42,7 +42,7 @@ abstract class ServerTestCase extends TestCase public function setUp() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->project = new Project($client); $this->textDocument = new Server\TextDocument($this->project, $client); $this->workspace = new Server\Workspace($this->project, $client); diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index 4f18473..ea62f70 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -12,7 +12,7 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 291e9d8..0ee62ef 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -19,7 +19,7 @@ class DidChangeTest extends TestCase { public function test() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $textDocument = new Server\TextDocument($project, $client); $phpDocument = $project->openDocument('whatever', "openDocument('whatever', 'hello world'); diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index 4d8ef24..ca6fc2c 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -18,14 +18,14 @@ class FormattingTest extends TestCase public function setUp() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $this->textDocument = new Server\TextDocument($project, $client); } public function testFormatting() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $textDocument = new Server\TextDocument($project, $client); $path = realpath(__DIR__ . '/../../../fixtures/format.php'); diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index 09efec7..128d58f 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity}; +use Sabre\Event\Promise; class ParseErrorsTest extends TestCase { @@ -19,17 +20,18 @@ class ParseErrorsTest extends TestCase public function setUp() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client->textDocument = new class($this->args) extends Client\TextDocument { private $args; public function __construct(&$args) { - parent::__construct(new MockProtocolStream()); + parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream)); $this->args = &$args; } - public function publishDiagnostics(string $uri, array $diagnostics) + public function publishDiagnostics(string $uri, array $diagnostics): Promise { $this->args = func_get_args(); + return Promise\resolve(null); } }; $project = new Project($client); diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 69eea36..6862d97 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -13,7 +13,7 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { - $client = new LanguageClient(new MockProtocolStream()); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $project = new Project($client); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));