diff --git a/composer.json b/composer.json index d6ea39c..3a8bd63 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "webmozart/glob": "^4.1", "sabre/uri": "^2.0", "JetBrains/phpstorm-stubs": "dev-master", - "composer/composer": "^1.3" + "composer/composer": "^1.3", + "reactivex/rxphp": "^1.5" }, "repositories": [ { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 7e856b1..9d069d6 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -122,40 +122,66 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->shutdown(); $this->exit(); }); - $this->protocolReader->on('message', function (Message $msg) { - coroutine(function () use ($msg) { - // Ignore responses, this is the handler for requests and notifications - if (AdvancedJsonRpc\Response::isResponse($msg->body)) { + // Map from request ID to subscription + $subscriptions = []; + $this->protocolReader->on('message', function (Message $msg) use ($subscriptions) { + // Ignore responses, this is the handler for requests and notifications + if (AdvancedJsonRpc\Response::isResponse($msg->body)) { + return; + } + if ($msg->body->method === '$/cancelRequest') { + if (!isset($subscriptions[$msg->body->id])) { return; } - $result = null; - $error = null; - try { - // Invoke the method handler to get a result - $result = yield $this->dispatch($msg->body); - } catch (AdvancedJsonRpc\Error $e) { - // If a ResponseError is thrown, send it back in the Response - $error = $e; - } catch (Throwable $e) { - // If an unexpected error occured, send back an INTERNAL_ERROR error response - $error = new AdvancedJsonRpc\Error( - (string)$e, - AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, - null, - $e + // Express that we are not interested anymore in the observable + $subscriptions[$msg->body->id]->dispose(); + return; + } + // The result object that is built through JSON patches + $result = null; + $error = null; + try { + // Invoke the method handler to get a result + $obs = $this->dispatch($msg->body); + } catch (\Throwable $e) { + $obs = Observable::error($e); + } + // Notifications dont need further acting + if (AdvancedJsonRpc\Notification::isNotification($msg->body)) { + return; + } + if (!($obs instanceof ObservableInterface)) { + $obs = Observable::just($obs); + } + $subscriptions[$msg->body->id] = $obs->subscribe(new CallbackObserver( + function (JSONPatch $patch) use (&$result) { + $this->protocolWriter->write( + new Message( + new AdvancedJsonRpc\Notification( + '$/partialResult', ['id' => $msg->body->id, 'patch' => $patch] + ) + ) ); - } - // Only send a Response for a Request - // Notifications do not send Responses - if (AdvancedJsonRpc\Request::isRequest($msg->body)) { - if ($error !== null) { - $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); - } else { - $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); + // Apply path to result object for BC + $patch->apply($result); + }, + function (\Exception $error) use ($msg) { + if (!($error instanceof AdvancedJsonRpc\Error)) { + $error = new AdvancedJsonRpc\Error( + (string)$error, + AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, + null, + $error + ); } - $this->protocolWriter->write(new Message($responseBody)); + // If an unexpected error occured, send back an INTERNAL_ERROR error response + $this->protocolWriter->write(new Message(new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error))); + }, + function () use ($msg, &$result) { + // Return the built result object for BC + $this->protocolWriter->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result))); } - })->otherwise('\\LanguageServer\\crash'); + )); }); $this->protocolWriter = $writer; $this->client = new LanguageClient($reader, $writer); diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 5aae7cf..b1e4f3d 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -62,10 +62,31 @@ class Workspace * The workspace symbol request is sent from the client to the server to list project-wide symbols matching the query string. * * @param string $query - * @return Promise + * @return Observable */ - public function symbol(string $query): Promise + public function symbol(string $query): Observable { + ( + $this->index->isStaticComplete() + ? observableFromEvent($this->index->once('static-complete', function () { + + }) + : Observable::empty() + ) + return Observable::create(function (ObserverInterface $observer) { + // Wait until indexing for definitions finished + if (!$this->index->isStaticComplete()) { + $this->index->once('static-complete', function () { + + }); + } + $observer->onNext(42); + $observer->onCompleted(); + + return new CallbackDisposable(function () { + + }); + }); return coroutine(function () use ($query) { // Wait until indexing for definitions finished if (!$this->index->isStaticComplete()) { diff --git a/src/utils.php b/src/utils.php index 8ab7beb..e4655d9 100644 --- a/src/utils.php +++ b/src/utils.php @@ -93,6 +93,20 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise return $p; } +/** + * Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter + * + * @param EmitterInterface $emitter + * @param string $event + * @return Promise + */ +function observableFromEvent(EmitterInterface $emitter, string $event): Promise +{ + Observable::create() + $emitter->once($event, [$p, 'fulfill']); + return $p; +} + /** * Returns the closest node of a specific type *