1
0
Fork 0

Handle Client responses (#128)

pull/38/head
Felix Becker 2016-10-31 11:47:21 +01:00 committed by GitHub
parent ff0a35d833
commit 04ef6c8adf
24 changed files with 233 additions and 110 deletions

View File

@ -3,9 +3,9 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use AdvancedJsonRpc\Notification as NotificationBody; use LanguageServer\ClientHandler;
use LanguageServer\ProtocolWriter;
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use Sabre\Event\Promise;
/** /**
* Provides method handlers for all textDocument/* methods * Provides method handlers for all textDocument/* methods
@ -13,13 +13,13 @@ use LanguageServer\Protocol\Message;
class TextDocument 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 string $uri
* @param Diagnostic[] $diagnostics * @param Diagnostic[] $diagnostics
* @return Promise <void>
*/ */
public function publishDiagnostics(string $uri, array $diagnostics) public function publishDiagnostics(string $uri, array $diagnostics): Promise
{ {
$this->protocolWriter->write(new Message(new NotificationBody( return $this->handler->notify('textDocument/publishDiagnostics', [
'textDocument/publishDiagnostics', 'uri' => $uri,
(object)[ 'diagnostics' => $diagnostics
'uri' => $uri, ]);
'diagnostics' => $diagnostics
]
)));
} }
} }

View File

@ -3,9 +3,9 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use AdvancedJsonRpc\Notification as NotificationBody; use LanguageServer\ClientHandler;
use LanguageServer\ProtocolWriter;
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use Sabre\Event\Promise;
/** /**
* Provides method handlers for all window/* methods * Provides method handlers for all window/* methods
@ -13,30 +13,26 @@ use LanguageServer\Protocol\Message;
class Window 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 int $type
* @param string $message * @param string $message
* @return Promise <void>
*/ */
public function showMessage(int $type, string $message) public function showMessage(int $type, string $message): Promise
{ {
$this->protocolWriter->write(new Message(new NotificationBody( return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]);
'window/showMessage',
(object)[
'type' => $type,
'message' => $message
]
)));
} }
/** /**
@ -44,15 +40,10 @@ class Window
* *
* @param int $type * @param int $type
* @param string $message * @param string $message
* @return Promise <void>
*/ */
public function logMessage(int $type, string $message) public function logMessage(int $type, string $message): Promise
{ {
$this->protocolWriter->write(new Message(new NotificationBody( return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]);
'window/logMessage',
(object)[
'type' => $type,
'message' => $message
]
)));
} }
} }

81
src/ClientHandler.php Normal file
View File

@ -0,0 +1,81 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use AdvancedJsonRpc;
use Sabre\Event\Promise;
class ClientHandler
{
/**
* @var ProtocolReader
*/
public $protocolReader;
/**
* @var ProtocolWriter
*/
public $protocolWriter;
/**
* @var IdGenerator
*/
public $idGenerator;
public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
{
$this->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 <mixed> 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) {
$promise = new Promise;
$listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) {
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
// Received a response
$this->protocolReader->removeListener('message', $listener);
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
$promise->fulfill($msg->body->result);
} else {
$promise->reject($msg->body->error);
}
}
};
$this->protocolReader->on('message', $listener);
return $promise;
});
}
/**
* Sends a notification to the client
*
* @param string $method The method to call
* @param array|object $params The method parameters
* @return Promise <null> 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)
)
);
}
}

25
src/IdGenerator.php Normal file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
/**
* Generates unique, incremental IDs for use as request IDs
*/
class IdGenerator
{
/**
* @var int
*/
public $counter = 1;
/**
* Returns a unique ID
*
* @return int
*/
public function generate()
{
return $this->counter++;
}
}

View File

@ -3,9 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Client\TextDocument;
use LanguageServer\Client\Window;
class LanguageClient class LanguageClient
{ {
/** /**
@ -22,12 +19,11 @@ class LanguageClient
*/ */
public $window; public $window;
private $protocolWriter; public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
public function __construct(ProtocolWriter $writer)
{ {
$this->protocolWriter = $writer; $handler = new ClientHandler($reader, $writer);
$this->textDocument = new TextDocument($writer);
$this->window = new Window($writer); $this->textDocument = new Client\TextDocument($handler);
$this->window = new Client\Window($handler);
} }
} }

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Server\TextDocument;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
ServerCapabilities, ServerCapabilities,
ClientCapabilities, ClientCapabilities,
@ -15,7 +14,6 @@ use LanguageServer\Protocol\{
}; };
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\Loop; use Sabre\Event\Loop;
use JsonMapper;
use Exception; use Exception;
use Throwable; use Throwable;
@ -56,7 +54,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
{ {
parent::__construct($this, '/'); parent::__construct($this, '/');
$this->protocolReader = $reader; $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; $result = null;
$error = null; $error = null;
try { try {
@ -81,7 +83,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} }
}); });
$this->protocolWriter = $writer; $this->protocolWriter = $writer;
$this->client = new LanguageClient($writer); $this->client = new LanguageClient($reader, $writer);
$this->project = new Project($this->client); $this->project = new Project($this->client);

View File

@ -3,7 +3,13 @@ declare(strict_types = 1);
namespace LanguageServer; 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);
} }

View File

@ -5,9 +5,9 @@ namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use AdvancedJsonRpc\Message as MessageBody; 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_HEADERS = 1;
const PARSE_BODY = 2; const PARSE_BODY = 2;
@ -17,7 +17,6 @@ class ProtocolStreamReader implements ProtocolReader
private $buffer = ''; private $buffer = '';
private $headers = []; private $headers = [];
private $contentLength; private $contentLength;
private $listener;
/** /**
* @param resource $input * @param resource $input
@ -43,11 +42,8 @@ class ProtocolStreamReader implements ProtocolReader
break; break;
case self::PARSE_BODY: case self::PARSE_BODY:
if (strlen($this->buffer) === $this->contentLength) { if (strlen($this->buffer) === $this->contentLength) {
if (isset($this->listener)) { $msg = new Message(MessageBody::parse($this->buffer), $this->headers);
$msg = new Message(MessageBody::parse($this->buffer), $this->headers); $this->emit('message', [$msg]);
$listener = $this->listener;
$listener($msg);
}
$this->parsingMode = self::PARSE_HEADERS; $this->parsingMode = self::PARSE_HEADERS;
$this->headers = []; $this->headers = [];
$this->buffer = ''; $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;
}
} }

View File

@ -31,12 +31,9 @@ class ProtocolStreamWriter implements ProtocolWriter
} }
/** /**
* Sends a Message to the client * {@inheritdoc}
*
* @param Message $msg
* @return Promise Resolved when the message has been fully written out to the output stream
*/ */
public function write(Message $msg) public function write(Message $msg): Promise
{ {
// if the message queue is currently empty, register a write handler. // if the message queue is currently empty, register a write handler.
if (empty($this->messages)) { if (empty($this->messages)) {

View File

@ -4,8 +4,15 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use Sabre\Event\Promise;
interface ProtocolWriter 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;
} }

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use AdvancedJsonRpc;
use Sabre\Event\Loop;
class ClientHandlerTest extends TestCase
{
public function testRequest()
{
$reader = new MockProtocolStream;
$writer = new MockProtocolStream;
$handler = new ClientHandler($reader, $writer);
$writer->once('message', function (Message $msg) use ($reader) {
// Respond to request
Loop\setTimeout(function () use ($reader, $msg) {
$reader->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, 'pong')));
}, 0);
});
$handler->request('testMethod', ['ping'])->then(function ($result) {
$this->assertEquals('pong', $result);
})->wait();
// No event listeners
$this->assertEquals([], $reader->listeners('message'));
$this->assertEquals([], $writer->listeners('message'));
}
public function testNotify()
{
$reader = new MockProtocolStream;
$writer = new MockProtocolStream;
$handler = new ClientHandler($reader, $writer);
$handler->notify('testMethod', ['ping'])->wait();
// No event listeners
$this->assertEquals([], $reader->listeners('message'));
$this->assertEquals([], $writer->listeners('message'));
}
}

View File

@ -16,7 +16,7 @@ class LanguageServerTest extends TestCase
$writer = new MockProtocolStream(); $writer = new MockProtocolStream();
$server = new LanguageServer($reader, $writer); $server = new LanguageServer($reader, $writer);
$msg = null; $msg = null;
$writer->onMessage(function (Message $message) use (&$msg) { $writer->on('message', function (Message $message) use (&$msg) {
$msg = $message; $msg = $message;
}); });
$reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [
@ -24,7 +24,7 @@ class LanguageServerTest extends TestCase
'processId' => getmypid(), 'processId' => getmypid(),
'capabilities' => new ClientCapabilities() '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->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body);
$this->assertEquals((object)[ $this->assertEquals((object)[
'capabilities' => (object)[ 'capabilities' => (object)[

View File

@ -5,34 +5,22 @@ namespace LanguageServer\Tests;
use LanguageServer\{ProtocolReader, ProtocolWriter}; use LanguageServer\{ProtocolReader, ProtocolWriter};
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use Sabre\Event\{Emitter, Promise};
/** /**
* A fake duplex protocol stream * 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 * Sends a Message to the client
* *
* @param Message $msg * @param Message $msg
* @return void * @return void
*/ */
public function write(Message $msg) public function write(Message $msg): Promise
{ {
if (isset($this->listener)) { $this->emit('message', [Message::parse((string)$msg)]);
$listener = $this->listener; return Promise\resolve(null);
$listener(Message::parse((string)$msg));
}
}
/**
* @param callable $listener Is called with a Message object
* @return void
*/
public function onMessage(callable $listener)
{
$this->listener = $listener;
} }
} }

View File

@ -15,7 +15,7 @@ class DefinitionCollectorTest extends TestCase
{ {
public function testCollectsSymbols() public function testCollectsSymbols()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php'));
@ -54,7 +54,7 @@ class DefinitionCollectorTest extends TestCase
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));

View File

@ -19,7 +19,7 @@ class PhpDocumentTest extends TestCase
public function setUp() public function setUp()
{ {
$this->project = new Project(new LanguageClient(new MockProtocolStream())); $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
} }
public function testParsesVariableVariables() public function testParsesVariableVariables()

View File

@ -19,7 +19,7 @@ class ProjectTest extends TestCase
public function setUp() public function setUp()
{ {
$this->project = new Project(new LanguageClient(new MockProtocolStream())); $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
} }
public function testGetDocumentLoadsDocument() public function testGetDocumentLoadsDocument()

View File

@ -18,7 +18,7 @@ class ProtocolStreamReaderTest extends TestCase
$writeHandle = fopen($tmpfile, 'w'); $writeHandle = fopen($tmpfile, 'w');
$reader = new ProtocolStreamReader(fopen($tmpfile, 'r')); $reader = new ProtocolStreamReader(fopen($tmpfile, 'r'));
$msg = null; $msg = null;
$reader->onMessage(function (Message $message) use (&$msg) { $reader->on('message', function (Message $message) use (&$msg) {
$msg = $message; $msg = $message;
}); });
$ret = fwrite($writeHandle, (string)new Message(new RequestBody(1, 'aMethod', ['arg' => 'Hello World']))); $ret = fwrite($writeHandle, (string)new Message(new RequestBody(1, 'aMethod', ['arg' => 'Hello World'])));

View File

@ -42,7 +42,7 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client); $this->project = new Project($client);
$this->textDocument = new Server\TextDocument($this->project, $client); $this->textDocument = new Server\TextDocument($this->project, $client);
$this->workspace = new Server\Workspace($this->project, $client); $this->workspace = new Server\Workspace($this->project, $client);

View File

@ -12,7 +12,7 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client); $this->textDocument = new Server\TextDocument($project, $client);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));

View File

@ -19,7 +19,7 @@ class DidChangeTest extends TestCase
{ {
public function test() public function test()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$textDocument = new Server\TextDocument($project, $client); $textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n"); $phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n");

View File

@ -13,7 +13,7 @@ class DidCloseTest extends TestCase
{ {
public function test() public function test()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$textDocument = new Server\TextDocument($project, $client); $textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->openDocument('whatever', 'hello world'); $phpDocument = $project->openDocument('whatever', 'hello world');

View File

@ -18,14 +18,14 @@ class FormattingTest extends TestCase
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client); $this->textDocument = new Server\TextDocument($project, $client);
} }
public function testFormatting() public function testFormatting()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$textDocument = new Server\TextDocument($project, $client); $textDocument = new Server\TextDocument($project, $client);
$path = realpath(__DIR__ . '/../../../fixtures/format.php'); $path = realpath(__DIR__ . '/../../../fixtures/format.php');

View File

@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity};
use Sabre\Event\Promise;
class ParseErrorsTest extends TestCase class ParseErrorsTest extends TestCase
{ {
@ -19,17 +20,18 @@ class ParseErrorsTest extends TestCase
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$client->textDocument = new class($this->args) extends Client\TextDocument { $client->textDocument = new class($this->args) extends Client\TextDocument {
private $args; private $args;
public function __construct(&$args) public function __construct(&$args)
{ {
parent::__construct(new MockProtocolStream()); parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream));
$this->args = &$args; $this->args = &$args;
} }
public function publishDiagnostics(string $uri, array $diagnostics) public function publishDiagnostics(string $uri, array $diagnostics): Promise
{ {
$this->args = func_get_args(); $this->args = func_get_args();
return Promise\resolve(null);
} }
}; };
$project = new Project($client); $project = new Project($client);

View File

@ -13,7 +13,7 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client); $project = new Project($client);
$this->textDocument = new Server\TextDocument($project, $client); $this->textDocument = new Server\TextDocument($project, $client);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));