1
0
Fork 0
observables
Felix Becker 2017-01-30 11:42:17 +01:00
parent 14ff2c46b0
commit bd4e20ac13
19 changed files with 369 additions and 109 deletions

View File

@ -36,7 +36,7 @@ class TextDocument
* @param Diagnostic[] $diagnostics * @param Diagnostic[] $diagnostics
* @return Promise <void> * @return Promise <void>
*/ */
public function publishDiagnostics(string $uri, array $diagnostics): Promise public function publishDiagnostics(string $uri, array $diagnostics): Observable
{ {
return $this->handler->notify('textDocument/publishDiagnostics', [ return $this->handler->notify('textDocument/publishDiagnostics', [
'uri' => $uri, 'uri' => $uri,
@ -51,13 +51,11 @@ class TextDocument
* @param TextDocumentIdentifier $textDocument The document to get the content for * @param TextDocumentIdentifier $textDocument The document to get the content for
* @return Promise <TextDocumentItem> The document's current content * @return Promise <TextDocumentItem> The document's current content
*/ */
public function xcontent(TextDocumentIdentifier $textDocument): Promise public function xcontent(TextDocumentIdentifier $textDocument): Observable
{ {
return $this->handler->request( return $this->handler->request(
'textDocument/xcontent', 'textDocument/xcontent',
['textDocument' => $textDocument] ['textDocument' => $textDocument]
)->then(function ($result) { );
return $this->mapper->map($result, new TextDocumentItem);
});
} }
} }

View File

@ -30,7 +30,7 @@ class Window
* @param string $message * @param string $message
* @return Promise <void> * @return Promise <void>
*/ */
public function showMessage(int $type, string $message): Promise public function showMessage(int $type, string $message): Observable
{ {
return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]); return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]);
} }
@ -42,7 +42,7 @@ class Window
* @param string $message * @param string $message
* @return Promise <void> * @return Promise <void>
*/ */
public function logMessage(int $type, string $message): Promise public function logMessage(int $type, string $message): Observable
{ {
return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]); return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]);
} }

View File

@ -33,15 +33,13 @@ class Workspace
* Returns a list of all files in a directory * Returns a list of all files in a directory
* *
* @param string $base The base directory (defaults to the workspace) * @param string $base The base directory (defaults to the workspace)
* @return Promise <TextDocumentIdentifier[]> Array of documents * @return Observable Emits JSON Patches that eventually result in TextDocumentIdentifier[]
*/ */
public function xfiles(string $base = null): Promise public function xfiles(string $base = null): Observable
{ {
return $this->handler->request( return $this->handler->request(
'workspace/xfiles', 'workspace/xfiles',
['base' => $base] ['base' => $base]
)->then(function (array $textDocuments) { );
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
});
} }
} }

View File

@ -3,8 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use AdvancedJsonRpc; use AdvancedJsonRpc as JsonRpc;
use Sabre\Event\Promise; use Rx\Observable;
class ClientHandler class ClientHandler
{ {
@ -35,30 +35,37 @@ class ClientHandler
* *
* @param string $method The method to call * @param string $method The method to call
* @param array|object $params The method parameters * @param array|object $params The method parameters
* @return Promise <mixed> Resolved with the result of the request or rejected with an error * @return Observable Emits JSON Patch operations for the result
*/ */
public function request(string $method, $params): Promise public function request(string $method, $params): Observable
{ {
$id = $this->idGenerator->generate(); $id = $this->idGenerator->generate();
return Observable::defer(function () {
return $this->protocolWriter->write( return $this->protocolWriter->write(
new Protocol\Message( new Protocol\Message(
new AdvancedJsonRpc\Request($id, $method, (object)$params) new AdvancedJsonRpc\Request($id, $method, (object)$params)
) )
)->then(function () use ($id) { );
$promise = new Promise; })
$listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) { // Wait for completion
->toArray()
// Subscribe to message events
->flatMap(function () {
return observableFromEvent($this->protocolReader, 'message');
})
->flatMap(function (JsonRpc\Message $msg) {
if (JsonRpc\Request::isRequest($msg->body) && $msg->body->method === '$/partialResult' && $msg->body->params->id === $id) {
return Observable::fromArray($msg->body->params->patch)->map(function ($operation) {
return Operation::fromDecodedJson($operation);
});
}
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) { if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
// Received a response
$this->protocolReader->removeListener('message', $listener);
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) { if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
$promise->fulfill($msg->body->result); return Observable::just(new Operation\Replace('/', $msg->body->result));
} else {
$promise->reject($msg->body->error);
} }
return Observable::error($msg->body->error);
} }
}; return Observable::emptyObservable();
$this->protocolReader->on('message', $listener);
return $promise;
}); });
} }
@ -67,9 +74,9 @@ class ClientHandler
* *
* @param string $method The method to call * @param string $method The method to call
* @param array|object $params The method parameters * @param array|object $params The method parameters
* @return Promise <null> Will be resolved as soon as the notification has been sent * @return Observable Will complete as soon as the notification has been sent
*/ */
public function notify(string $method, $params): Promise public function notify(string $method, $params): Observable
{ {
$id = $this->idGenerator->generate(); $id = $this->idGenerator->generate();
return $this->protocolWriter->write( return $this->protocolWriter->write(

View File

@ -24,12 +24,12 @@ class ClientContentRetriever implements ContentRetriever
* Retrieves the content of a text document identified by the URI through a textDocument/xcontent request * Retrieves the content of a text document identified by the URI through a textDocument/xcontent request
* *
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string * @return Observable <string> Emits the content as a string
*/ */
public function retrieve(string $uri): Promise public function retrieve(string $uri): Observable
{ {
return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)) return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))
->then(function (TextDocumentItem $textDocument) { ->map(function (TextDocumentItem $textDocument) {
return $textDocument->text; return $textDocument->text;
}); });
} }

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\ContentRetriever; namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Rx\Observable;
/** /**
* Interface for retrieving the content of a text document * Interface for retrieving the content of a text document
@ -14,7 +15,7 @@ interface ContentRetriever
* Retrieves the content of a text document identified by the URI * Retrieves the content of a text document identified by the URI
* *
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string * @return Observable <string> Emits the content as a string
*/ */
public function retrieve(string $uri): Promise; public function retrieve(string $uri): Observable;
} }

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\ContentRetriever; namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise; use Rx\Observable;
use function LanguageServer\uriToPath; use function LanguageServer\uriToPath;
/** /**
@ -15,10 +15,10 @@ class FileSystemContentRetriever implements ContentRetriever
* Retrieves the content of a text document identified by the URI from the file system * Retrieves the content of a text document identified by the URI from the file system
* *
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string * @return Observable Emits the content as a string
*/ */
public function retrieve(string $uri): Promise public function retrieve(string $uri): Observable
{ {
return Promise\resolve(file_get_contents(uriToPath($uri))); return Observable::just(file_get_contents(uriToPath($uri)));
} }
} }

View File

@ -31,11 +31,14 @@ class ClientFilesFinder implements FilesFinder
* If the client does not support workspace/files, it falls back to searching the file system directly. * If the client does not support workspace/files, it falls back to searching the file system directly.
* *
* @param string $glob * @param string $glob
* @return Promise <string[]> The URIs * @return Observable Emits each URI
*/ */
public function find(string $glob): Promise public function find(string $glob): Observable
{ {
return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) { return $this->client->workspace->xfiles()
->flatMap(function (Operation $operation) use ($glob) {
$uris = []; $uris = [];
foreach ($textDocuments as $textDocument) { foreach ($textDocuments as $textDocument) {
$path = Uri\parse($textDocument->uri)['path']; $path = Uri\parse($textDocument->uri)['path'];

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace JsonPatch;
class Operation
{
/**
* @var Pointer
*/
public $path;
public function __construct(Pointer $path)
{
$this->path = $path;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace JsonPatch\Operation;
/**
* Adds a value to an object or inserts it into an array.
* In the case of an array the value is inserted before the given index.
* The - character can be used instead of an index to insert at the end of an array.
*/
class Add extends Operation
{
/**
* @var mixed
*/
public $value;
public function __construct(Pointer $path, $value)
{
parent::__construct($path);
$this->value = $value;
}
/**
* @param mixed $target
* @return void
*/
public function apply(&$target)
{
if (is_array($this->path->parent->at($target))) {
// Numeric key
if ($this->path->key === 0) {
throw new \Exception('Cannot add before 0');
}
$this->path->parent->go($this->path->key - 1)->at($target) = $this->value;
}
$this->path->at($target);
}
}

16
src/JsonPatch/Patch.php Normal file
View File

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
class Patch
{
public function __construct(Operation[] $operations)
{
}
public function apply(mixed $target)
{
}
}

97
src/JsonPatch/Pointer.php Normal file
View File

@ -0,0 +1,97 @@
<?php
declare(strict_types = 1);
namespace JsonPatch;
class Pointer
{
/**
* @var self|null
*/
public $parent;
/**
* The property name or array index. The root pointer has an empty key
*
* @var string
*/
public $key;
/**
* @var string $pointer A full JSON Pointer string
* @return self
*/
public static function parse(string $pointer)
{
$p = new self(null, '');
// TODO unescape
foreach (explode('/', $pointer) as $key) {
if ((string)(int)$key === $key) {
$key = (int)$key;
}
$p = new self($p, $key);
}
return $p;
}
/**
* @param self $parent
* @param string|number $key
*/
public function __construct($parent, $key)
{
if (!is_int($key) && !is_string($key)) {
throw new \IllegalArgumentException('Key must be string or int');
}
$this->parent = $parent;
$this->key = $key;
}
/**
* Returns a reference to the value the pointer points to at the target
*
* @param object|array $target
*/
public function &at($target)
{
if ($this->parent !== null) {
$target = $this->parent->at($target);
}
$key = $this->key;
if ($key === '') {
return $target;
}
if ($key === '-') {
if (!is_array($target)) {
throw new \Exception('Trying to apply "-" on a non-array');
}
$key = count($target);
}
if (is_array($target)) {
return $target[$key];
}
return &$target->$key;
}
/**
* @param string|int $key
* @return self
*/
public function go($key)
{
if (!is_int($key) && !is_string($key)) {
throw new \IllegalArgumentException('Key must be string or int');
}
return new self($this, $key);
}
/**
* @return string
*/
public function __toString()
{
if ($this->parent !== null && $this->parent->key !== '') {
return (string)($this->parent ?? '') . '/' . $this->key;
}
}
}

View File

@ -336,7 +336,10 @@ class LanguageServer extends JsonRpc\Dispatcher
return coroutine(function () use ($rootPath) { return coroutine(function () use ($rootPath) {
$pattern = Path::makeAbsolute('**/*.php', $rootPath); $pattern = Path::makeAbsolute('**/*.php', $rootPath);
$uris = yield $this->filesFinder->find($pattern); $this->filesFinder->find($pattern)
->flatMap(function (Operation $op) {
if ($op instanceof Operation\Add && ($op->getPath() === '/' || $op->getPath() )
});
$count = count($uris); $count = count($uris);

View File

@ -80,11 +80,11 @@ class PhpDocumentLoader
* If the document is not open, loads it. * If the document is not open, loads it.
* *
* @param string $uri * @param string $uri
* @return Promise <PhpDocument> * @return Observable PhpDocument
*/ */
public function getOrLoad(string $uri): Promise public function getOrLoad(string $uri): Observable
{ {
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($uri); return isset($this->documents[$uri]) ? Observable::just($this->documents[$uri]) : $this->load($uri);
} }
/** /**
@ -93,14 +93,12 @@ class PhpDocumentLoader
* The document is NOT added to the list of open documents, but definitions are registered. * The document is NOT added to the list of open documents, but definitions are registered.
* *
* @param string $uri * @param string $uri
* @return Promise <PhpDocument> * @return Observable <PhpDocument>
*/ */
public function load(string $uri): Promise public function load(string $uri): Observable
{ {
return coroutine(function () use ($uri) { return $this->contentRetriever->retrieve($uri)->map(function (string $content) {
$limit = 150000; $limit = 150000;
$content = yield $this->contentRetriever->retrieve($uri);
$size = strlen($content); $size = strlen($content);
if ($size > $limit) { if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit); throw new ContentTooLargeException($uri, $size, $limit);

View File

@ -33,7 +33,7 @@ class ProtocolStreamWriter implements ProtocolWriter
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function write(Message $msg): Promise public function write(Message $msg): Observable
{ {
// 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)) {
@ -42,12 +42,12 @@ class ProtocolStreamWriter implements ProtocolWriter
}); });
} }
$promise = new Promise(); $subject = new Subject;
$this->messages[] = [ $this->messages[] = [
'message' => (string)$msg, 'message' => (string)$msg,
'promise' => $promise 'subject' => $subject
]; ];
return $promise; return $subject->asObservable();
} }
/** /**
@ -60,7 +60,7 @@ class ProtocolStreamWriter implements ProtocolWriter
$keepWriting = true; $keepWriting = true;
while ($keepWriting) { while ($keepWriting) {
$message = $this->messages[0]['message']; $message = $this->messages[0]['message'];
$promise = $this->messages[0]['promise']; $subject = $this->messages[0]['subject'];
$bytesWritten = @fwrite($this->output, $message); $bytesWritten = @fwrite($this->output, $message);
@ -78,7 +78,7 @@ class ProtocolStreamWriter implements ProtocolWriter
$keepWriting = false; $keepWriting = false;
} }
$promise->fulfill(); $subject->onComplete();
} else { } else {
$this->messages[0]['message'] = $message; $this->messages[0]['message'] = $message;
$keepWriting = false; $keepWriting = false;

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Protocol\Message;
use Sabre\Event\Promise; use Rx\Observable;
interface ProtocolWriter interface ProtocolWriter
{ {
@ -12,7 +12,7 @@ interface ProtocolWriter
* Sends a Message to the client * Sends a Message to the client
* *
* @param Message $msg * @param Message $msg
* @return Promise Resolved when the message has been fully written out to the output stream * @return Observable Resolved when the message has been fully written out to the output stream
*/ */
public function write(Message $msg): Promise; public function write(Message $msg): Observable;
} }

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace Rx\Operator;
use gamringer\JSONPatch\Patch;
class ApplyJsonPatchesOperator extends Operator
{
private $classType;
private $isArray;
private $mapper;
public function __construct(JsonMapper $mapper, string $classType, bool $isArray = false)
{
$this->classType = $classType;
$this->isArray = $isArray;
$this->mapper = $mapper;
}
/**
* @param ObservableInterface $observable
* @param ObserverInterface $observer
* @param SchedulerInterface|null $scheduler
* @return \Rx\DisposableInterface
*/
public function __invoke(ObservableInterface $observable, ObserverInterface $observer, SchedulerInterface $scheduler = null)
{
$result = null;
$pointer = new Pointer($result);
return $observable->subscribe(new CallbackObserver(
function (JsonPatch $patch) use ($pointer) {
$patch->apply($pointer);
if ($this->isArray) {
$result = [];
} else {
$classType = $this->classType;
$result = new $classType;
}
}),
[$observer, 'onError'],
function () use (&$result) {
$observer->onNext($result);
$observer->onComplete();
}
);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
namespace Rx\Operator;
use gamringer\JSONPatch\Patch;
class ApplyJsonPatchesOperator extends Operator
{
/**
* @param ObservableInterface $observable
* @param ObserverInterface $observer
* @param SchedulerInterface|null $scheduler
* @return \Rx\DisposableInterface
*/
public function __invoke(ObservableInterface $observable, ObserverInterface $observer, SchedulerInterface $scheduler = null)
{
$result = null;
$pointer = new Pointer($result);
return $observable->subscribe(new CallbackObserver(
function (JsonPatch $patch) use ($pointer) {
$patch->apply($pointer);
}),
[$observer, 'onError'],
function () use (&$result) {
$observer->onNext($result);
$observer->onComplete();
}
);
}
}

View File

@ -385,11 +385,11 @@ class TextDocument
*/ */
public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): Promise public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return $this->documentLoader->getOrLoad($textDocument->uri)
$document = yield $this->documentLoader->getOrLoad($textDocument->uri); ->flatMap(function (PhpDocument $document) {
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return Observable::empty();
} }
// Handle definition nodes // Handle definition nodes
while (true) { while (true) {
@ -431,6 +431,7 @@ class TextDocument
$symbol->package = $this->composerJson; $symbol->package = $this->composerJson;
} }
return [new SymbolLocationInformation($symbol, $symbol->location)]; return [new SymbolLocationInformation($symbol, $symbol->location)];
})
}); });
} }
} }