1
0
Fork 0

Handle Client responses

pull/128/head
Felix Becker 2016-10-29 16:53:20 +02:00
parent bec24383d4
commit 9283ba2148
23 changed files with 190 additions and 110 deletions

View File

@ -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 <void>
*/
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)[
return $this->handler->notify('textDocument/publishDiagnostics', [
'uri' => $uri,
'diagnostics' => $diagnostics
]
)));
]);
}
}

View File

@ -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 <void>
*/
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 <void>
*/
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]);
}
}

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
*/
private $protocolReader;
/**
* @var ProtocolWriter
*/
private $protocolWriter;
/**
* @var IdGenerator
*/
private $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) {
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 <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;
use LanguageServer\Client\TextDocument;
use LanguageServer\Client\Window;
class LanguageClient
{
/**
@ -22,12 +19,11 @@ class LanguageClient
*/
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);
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}
$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;
}
}

View File

@ -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)) {

View File

@ -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;
}

View File

@ -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)[

View File

@ -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);
}
}

View File

@ -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'));

View File

@ -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()

View File

@ -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()

View File

@ -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'])));

View File

@ -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);

View File

@ -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'));

View File

@ -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', "<?php\necho 'Hello, World'\n");

View File

@ -13,7 +13,7 @@ class DidCloseTest 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', 'hello world');

View File

@ -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');

View File

@ -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);

View File

@ -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'));