1
0
Fork 0
observables
Felix Becker 2017-01-27 18:56:08 +01:00
parent f43c41739e
commit 9c590e38da
5 changed files with 178 additions and 156 deletions

View File

@ -38,7 +38,8 @@
"sabre/uri": "^2.0", "sabre/uri": "^2.0",
"JetBrains/phpstorm-stubs": "dev-master", "JetBrains/phpstorm-stubs": "dev-master",
"composer/composer": "^1.3", "composer/composer": "^1.3",
"reactivex/rxphp": "^1.5" "reactivex/rxphp": "^1.5",
"gamringer/php-json-patch": "^1.0"
}, },
"repositories": [ "repositories": [
{ {

View File

@ -17,16 +17,19 @@ use LanguageServer\Protocol\{
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use AdvancedJsonRpc; use AdvancedJsonRpc as JsonRpc;
use Sabre\Event\{Loop, Promise}; use Sabre\Event\{Loop, Promise};
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Rx\{Observable, CallbackObserver, ObservableInterface};
use gamringer\JSONPatch\{Patch, Operation};
use gamringer\JSONPointer\Pointer;
use Exception; use Exception;
use Throwable; use Throwable;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Webmozart\Glob\Glob; use Webmozart\Glob\Glob;
use Sabre\Uri; use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher class LanguageServer extends JsonRpc\Dispatcher
{ {
/** /**
* Handles textDocument/* method calls * Handles textDocument/* method calls
@ -124,22 +127,23 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
}); });
// Map from request ID to subscription // Map from request ID to subscription
$subscriptions = []; $subscriptions = [];
$this->protocolReader->on('message', function (Message $msg) use ($subscriptions) { $this->protocolReader->on('message', function (Message $msg) use (&$subscriptions) {
// Ignore responses, this is the handler for requests and notifications // Ignore responses, this is the handler for requests and notifications
if (AdvancedJsonRpc\Response::isResponse($msg->body)) { if (JsonRpc\Response::isResponse($msg->body)) {
return; return;
} }
if ($msg->body->method === '$/cancelRequest') { if ($msg->body->method === '$/cancelRequest') {
if (!isset($subscriptions[$msg->body->id])) { if (!isset($subscriptions[$msg->body->params->id])) {
return; return;
} }
// Express that we are not interested anymore in the observable // Express that we are not interested anymore in the observable
$subscriptions[$msg->body->id]->dispose(); $subscriptions[$msg->body->params->id]->dispose();
unset($msg->body->params->id);
return; return;
} }
// The result object that is built through JSON patches // The result object that is built through JSON patches
$result = null; $result = null;
$error = null; $pointer = new Pointer($result);
try { try {
// Invoke the method handler to get a result // Invoke the method handler to get a result
$obs = $this->dispatch($msg->body); $obs = $this->dispatch($msg->body);
@ -147,39 +151,35 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$obs = Observable::error($e); $obs = Observable::error($e);
} }
// Notifications dont need further acting // Notifications dont need further acting
if (AdvancedJsonRpc\Notification::isNotification($msg->body)) { if (JsonRpc\Notification::isNotification($msg->body)) {
return; return;
} }
if (!($obs instanceof ObservableInterface)) { if (!($obs instanceof ObservableInterface)) {
$obs = Observable::just($obs); $obs = Observable::just(new JSONPatch('replace', '/', $obs));
} }
$subscriptions[$msg->body->id] = $obs->subscribe(new CallbackObserver( $subscriptions[$msg->body->id] = $obs->subscribe(new CallbackObserver(
function (JSONPatch $patch) use (&$result) { function (Operation\Appliable $operation) use ($pointer) {
$this->protocolWriter->write( $this->protocolWriter->write(new Message(new JsonRpc\Notification('$/partialResult', [
new Message( 'id' => $msg->body->id,
new AdvancedJsonRpc\Notification( 'patch' => [$operation]
'$/partialResult', ['id' => $msg->body->id, 'patch' => $patch] ])));
)
)
);
// Apply path to result object for BC // Apply path to result object for BC
$patch->apply($result); $operation->apply($pointer);
}, },
function (\Exception $error) use ($msg) { function (\Exception $error) use ($msg) {
if (!($error instanceof AdvancedJsonRpc\Error)) { if (!($error instanceof JsonRpc\Error)) {
$error = new AdvancedJsonRpc\Error( $error = new JsonRpc\Error((string)$error, JsonRpc\ErrorCode::INTERNAL_ERROR, null, $error);
(string)$error,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null,
$error
);
} }
// If an unexpected error occured, send back an INTERNAL_ERROR error response // If an unexpected error occured, send back an INTERNAL_ERROR error response
$this->protocolWriter->write(new Message(new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error))); $this->protocolWriter->write(new Message(new JsonRpc\ErrorResponse($msg->body->id, $error)));
}, },
function () use ($msg, &$result) { function () use ($msg, &$result, &$subscriptions) {
// Return the built result object for BC // Return the complete result object for BC
$this->protocolWriter->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result))); $this->protocolWriter->write(new Message(new JsonRpc\SuccessResponse($msg->body->id, $result)));
if (isset($subscriptions[$msg->body->id]) {
$subscriptions[$msg->body->id]->dispose();
unset($subscriptions[$msg->body->id]);
}
} }
)); ));
}); });

View File

@ -260,40 +260,55 @@ class TextDocument
* *
* @param TextDocumentIdentifier $textDocument The text document * @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document * @param Position $position The position inside the text document
* @return Promise <Location|Location[]> * @return Observable Will emit JSON Patch operations that eventually result in Location[]
*/ */
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise public function definition(TextDocumentIdentifier $textDocument, Position $position): Observable
{ {
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
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = DefinitionResolver::getDefinedFqn($node);
while (true) { return Observable::merge(
observableFromEvent($this->index, 'definition-added')->throttle(100),
observableFromEvent($this->index, 'complete')->take(1)
)
// Repeat the following logic as long as the index is not complete
->takeWhile(function () {
return !$this->index->isComplete();
})
// Try to find the definition on each event
->map(function () use ($fqn) {
if ($fqn) { if ($fqn) {
$def = $this->index->getDefinition($fqn); return $this->index->getDefinition($fqn);
} else { } else {
// Handle reference nodes // Handle reference nodes
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); return $this->definitionResolver->resolveReferenceNodeToDefinition($node);
} }
// If no result was found and we are still indexing, try again after the index was updated
if ($def !== null || $this->index->isComplete()) {
break;
}
yield waitForEvent($this->index, 'definition-added');
}
if (
$def === null
|| $def->symbolInformation === null
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
) {
return [];
}
return $def->symbolInformation->location;
}); });
})
// Ignore events where definitions were not found
->filter(function (Definition $def) {
return $def === null;
})
// If we found one definition, complete
->take(1)
// Check if we actually have a Definition with location info and Definition is not part of stubs
->filter(function (Definition $def) {
return (
$def->symbolInformation === null
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
);
})
// Turn the found Definition into an add operation for the location
->map(function (Definition $def) {
return new Operation\Add('/-', $def->symbolInformation->location);
})
// Initialize with empty array
->startWith(new Operation\Replace('/', []));
} }
/** /**

View File

@ -7,6 +7,7 @@ use LanguageServer\{LanguageClient, Project, PhpDocumentLoader};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location}; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location};
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Rx\Observable;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use function LanguageServer\waitForEvent; use function LanguageServer\waitForEvent;
@ -62,44 +63,31 @@ class Workspace
* The workspace symbol request is sent from the client to the server to list project-wide symbols matching the query string. * The workspace symbol request is sent from the client to the server to list project-wide symbols matching the query string.
* *
* @param string $query * @param string $query
* @return Observable <SymbolInformation[]> * @return Observable Will yield JSON Patch Operations that eventually result in SymbolInformation[]
*/ */
public function symbol(string $query): Observable public function symbol(string $query): Observable
{ {
( return Observable::just(null)
$this->index->isStaticComplete() // Wait for indexing event if not yet finished
? observableFromEvent($this->index->once('static-complete', function () { ->flatMap(function () {
if (!$this->index->isStaticComplete()) {
return observableFromEvent($this->index, 'static-complete')->take(1);
}
}) })
: Observable::empty() // Get definitions from complete index
) ->flatMap(function () {
return Observable::create(function (ObserverInterface $observer) { return Observable::fromArray($this->index->getDefinitions());
// Wait until indexing for definitions finished })
if (!$this->index->isStaticComplete()) { // Filter by matching FQN to query
$this->index->once('static-complete', function () { ->filter(function (Definition $def) use ($query) {
return $query === '' || stripos($def->fqn, $query) !== false;
}); })
} // Send each SymbolInformation
$observer->onNext(42); ->map(function (Definition $def) use ($query) {
$observer->onCompleted(); return new Operation\Add('/-', $def->symbolInformation);
})
return new CallbackDisposable(function () { // Initialize with an empty array
->startWith(new Operation\Replace('/', []));
});
});
return coroutine(function () use ($query) {
// Wait until indexing for definitions finished
if (!$this->index->isStaticComplete()) {
yield waitForEvent($this->index, 'static-complete');
}
$symbols = [];
foreach ($this->index->getDefinitions() as $fqn => $definition) {
if ($query === '' || stripos($fqn, $query) !== false) {
$symbols[] = $definition->symbolInformation;
}
}
return $symbols;
});
} }
/** /**
@ -107,36 +95,36 @@ class Workspace
* *
* @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for. * @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for.
* @param string[] $files An optional list of files to restrict the search to. * @param string[] $files An optional list of files to restrict the search to.
* @return ReferenceInformation[] * @return Observable ReferenceInformation[]
*/ */
public function xreferences($query, array $files = null): Promise public function xreferences($query, array $files = null): Observable
{ {
return coroutine(function () use ($query, $files) { return Observable::just(null)
->flatMap(function () {
if ($this->composerLock === null) { if ($this->composerLock === null) {
return []; return Observable::empty();
} }
// Wait until indexing finished // Wait until indexing finished
if (!$this->index->isComplete()) { if (!$this->index->isComplete()) {
yield waitForEvent($this->index, 'complete'); return observableFromEvent($this->index, 'complete')->take(1);
} }
/** Map from URI to array of referenced FQNs in dependencies */ })
$refs = []; // Get all definition FQNs in dependencies
// Get all references TO dependencies ->flatMap(function () {
$fqns = isset($query->fqsen) ? [$query->fqsen] : array_values($this->dependenciesIndex->getDefinitions()); if (isset($query->fqsen)) {
foreach ($fqns as $fqn) { $fqns = [$this->dependenciesIndex->getDefinition($query->fqsen)];
foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) { } else {
if (!isset($refs[$uri])) { $fqns = $this->dependenciesIndex->getDefinitions();
$refs[$uri] = [];
} }
if (array_search($uri, $refs[$uri]) === false) { return Observable::fromArray($fqns);
$refs[$uri][] = $fqn; })
} // Get all URIs in the project source that reference those definitions
} ->flatMap(function (Definition $def) {
} return Observable::fromArray($this->sourceIndex->getReferenceUris($fqn));
$refInfos = []; })
foreach ($refs as $uri => $fqns) { ->distinct()
foreach ($fqns as $fqn) { ->flatMap(function (string $uri) {
$def = $this->dependenciesIndex->getDefinition($fqn); return $this->documentLoader->getOrLoad($uri);
$symbol = new SymbolDescriptor; $symbol = new SymbolDescriptor;
$symbol->fqsen = $fqn; $symbol->fqsen = $fqn;
foreach (get_object_vars($def->symbolInformation) as $prop => $val) { foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
@ -145,12 +133,13 @@ class Workspace
// Find out package name // Find out package name
preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches); preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches);
$packageName = $matches[1]; $packageName = $matches[1];
foreach ($this->composerLock->packages as $package) { foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
if ($package->name === $packageName) { if ($package->name === $packageName) {
$symbol->package = $package; $symbol->package = $package;
break; break;
} }
} }
})
// If there was no FQSEN provided, check if query attributes match // If there was no FQSEN provided, check if query attributes match
if (!isset($query->fqsen)) { if (!isset($query->fqsen)) {
$matches = true; $matches = true;
@ -164,17 +153,24 @@ class Workspace
continue; continue;
} }
} }
$doc = yield $this->documentLoader->getOrLoad($uri);
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) { foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
$refInfo = new ReferenceInformation; $refInfo = new ReferenceInformation;
$refInfo->reference = Location::fromNode($node); $refInfo->reference = Location::fromNode($node);
$refInfo->symbol = $symbol; $refInfo->symbol = $symbol;
$refInfos[] = $refInfo; $refInfos[] = $refInfo;
} }
})
->flatMap(function (PhpDocument $doc) use ($fqn) {
})
$refInfos = [];
foreach ($refs as $uri => $fqns) {
foreach ($fqns as $fqn) {
} }
} }
return $refInfos; return $refInfos;
}); })
->startWith(new Operation\Replace('/', []));
} }
/** /**

View File

@ -79,6 +79,13 @@ function timeout($seconds = 0): Promise
return $promise; return $promise;
} }
function observableTimeout($seconds = 0)
{
return Observable::create(function (ObserverInterface $observer) use ($seconds) {
Loop\setTimeout([$observer, 'onCompleted'], $seconds);
});
}
/** /**
* Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter * Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter
* *
@ -94,7 +101,7 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise
} }
/** /**
* Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter * Returns an Observable that emits every event. For once semantics use ->take(1)
* *
* @param EmitterInterface $emitter * @param EmitterInterface $emitter
* @param string $event * @param string $event
@ -102,9 +109,12 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise
*/ */
function observableFromEvent(EmitterInterface $emitter, string $event): Promise function observableFromEvent(EmitterInterface $emitter, string $event): Promise
{ {
Observable::create() Observable::create(function (Observer $observer) use ($emitter, $event) {
$emitter->once($event, [$p, 'fulfill']); $emitter->on($event, [$observer, 'onNext']);
return $p; return new CallbackDisposable(function () use ($event, $observer) {
return $emitter->removeListener($event, [$observer, 'onNext']);
});
});
} }
/** /**