From 96aa99848665dedb53e910a02802fe5b18665b36 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 25 Jan 2017 01:38:11 +0100 Subject: [PATCH] Make Index an EventEmitter (#255) --- src/ComposerScripts.php | 2 + src/Index/AbstractAggregateIndex.php | 84 +++++++++++++++++++ src/Index/DependenciesIndex.php | 4 +- src/Index/GlobalIndex.php | 1 + src/Index/Index.php | 59 +++++++++++++ src/Index/ProjectIndex.php | 1 + src/Index/ReadableIndex.php | 21 ++++- src/LanguageServer.php | 40 +++++++-- src/Server/TextDocument.php | 62 ++++++++++---- src/Server/Workspace.php | 27 ++++-- src/utils.php | 16 +++- tests/Server/ServerTestCase.php | 1 + .../Definition/GlobalFallbackTest.php | 1 + .../References/GlobalFallbackTest.php | 1 + tests/Server/Workspace/SymbolTest.php | 4 +- 15 files changed, 286 insertions(+), 38 deletions(-) diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php index d5b06ca..d9e11ab 100644 --- a/src/ComposerScripts.php +++ b/src/ComposerScripts.php @@ -58,6 +58,8 @@ class ComposerScripts $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); } + $index->setComplete(); + echo "Saving Index\n"; $index->save(); diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index f8934c6..5377c3a 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -4,9 +4,12 @@ declare(strict_types = 1); namespace LanguageServer\Index; use LanguageServer\Definition; +use Sabre\Event\EmitterTrait; abstract class AbstractAggregateIndex implements ReadableIndex { + use EmitterTrait; + /** * Returns all indexes managed by the aggregate index * @@ -14,6 +17,87 @@ abstract class AbstractAggregateIndex implements ReadableIndex */ abstract protected function getIndexes(): array; + public function __construct() + { + foreach ($this->getIndexes() as $index) { + $this->registerIndex($index); + } + } + + /** + * @param ReadableIndex $index + */ + protected function registerIndex(ReadableIndex $index) + { + $index->on('complete', function () { + if ($this->isComplete()) { + $this->emit('complete'); + } + }); + $index->on('static-complete', function () { + if ($this->isStaticComplete()) { + $this->emit('static-complete'); + } + }); + $index->on('definition-added', function () { + $this->emit('definition-added'); + }); + } + + /** + * Marks this index as complete + * + * @return void + */ + public function setComplete() + { + foreach ($this->getIndexes() as $index) { + $index->setComplete(); + } + } + + /** + * Marks this index as complete for static definitions and references + * + * @return void + */ + public function setStaticComplete() + { + foreach ($this->getIndexes() as $index) { + $index->setStaticComplete(); + } + } + + /** + * Returns true if this index is complete + * + * @return bool + */ + public function isComplete(): bool + { + foreach ($this->getIndexes() as $index) { + if (!$index->isComplete()) { + return false; + } + } + return true; + } + + /** + * Returns true if this index is complete for static definitions or references + * + * @return bool + */ + public function isStaticComplete(): bool + { + foreach ($this->getIndexes() as $index) { + if (!$index->isStaticComplete()) { + return false; + } + } + return true; + } + /** * Returns an associative array [string => Definition] that maps fully qualified symbol names * to Definitions diff --git a/src/Index/DependenciesIndex.php b/src/Index/DependenciesIndex.php index a355821..aa5e06e 100644 --- a/src/Index/DependenciesIndex.php +++ b/src/Index/DependenciesIndex.php @@ -27,7 +27,9 @@ class DependenciesIndex extends AbstractAggregateIndex public function getDependencyIndex(string $packageName): Index { if (!isset($this->indexes[$packageName])) { - $this->indexes[$packageName] = new Index; + $index = new Index; + $this->indexes[$packageName] = $index; + $this->registerIndex($index); } return $this->indexes[$packageName]; } diff --git a/src/Index/GlobalIndex.php b/src/Index/GlobalIndex.php index e1e6d48..f9f5300 100644 --- a/src/Index/GlobalIndex.php +++ b/src/Index/GlobalIndex.php @@ -26,6 +26,7 @@ class GlobalIndex extends AbstractAggregateIndex { $this->stubsIndex = $stubsIndex; $this->projectIndex = $projectIndex; + parent::__construct(); } /** diff --git a/src/Index/Index.php b/src/Index/Index.php index 29cbb99..183e283 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Index; use LanguageServer\Definition; +use Sabre\Event\EmitterTrait; /** * Represents the index of a project or dependency @@ -11,6 +12,8 @@ use LanguageServer\Definition; */ class Index implements ReadableIndex { + use EmitterTrait; + /** * An associative array that maps fully qualified symbol names to Definitions * @@ -25,6 +28,61 @@ class Index implements ReadableIndex */ private $references = []; + /** + * @var bool + */ + private $complete = false; + + /** + * @var bool + */ + private $staticComplete = false; + + /** + * Marks this index as complete + * + * @return void + */ + public function setComplete() + { + if (!$this->isStaticComplete()) { + $this->setStaticComplete(); + } + $this->complete = true; + $this->emit('complete'); + } + + /** + * Marks this index as complete for static definitions and references + * + * @return void + */ + public function setStaticComplete() + { + $this->staticComplete = true; + $this->emit('static-complete'); + } + + /** + * Returns true if this index is complete + * + * @return bool + */ + public function isComplete(): bool + { + return $this->complete; + } + + /** + * Returns true if this index is complete + * + * @return bool + */ + public function isStaticComplete(): bool + { + return $this->staticComplete; + } + /** * Returns an associative array [string => Definition] that maps fully qualified symbol names * to Definitions @@ -65,6 +123,7 @@ class Index implements ReadableIndex public function setDefinition(string $fqn, Definition $definition) { $this->definitions[$fqn] = $definition; + $this->emit('definition-added'); } /** diff --git a/src/Index/ProjectIndex.php b/src/Index/ProjectIndex.php index aa31b21..8adc938 100644 --- a/src/Index/ProjectIndex.php +++ b/src/Index/ProjectIndex.php @@ -26,6 +26,7 @@ class ProjectIndex extends AbstractAggregateIndex { $this->sourceIndex = $sourceIndex; $this->dependenciesIndex = $dependenciesIndex; + parent::__construct(); } /** diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php index 40f4e40..67b20b6 100644 --- a/src/Index/ReadableIndex.php +++ b/src/Index/ReadableIndex.php @@ -4,12 +4,31 @@ declare(strict_types = 1); namespace LanguageServer\Index; use LanguageServer\Definition; +use Sabre\Event\EmitterInterface; /** * The ReadableIndex interface provides methods to lookup definitions and references + * + * @event definition-added Emitted when a definition was added + * @event static-complete Emitted when definitions and static references are complete + * @event complete Emitted when the index is complete */ -interface ReadableIndex +interface ReadableIndex extends EmitterInterface { + /** + * Returns true if this index is complete + * + * @return bool + */ + public function isComplete(): bool; + + /** + * Returns true if definitions and static references are complete + * + * @return bool + */ + public function isStaticComplete(): bool; + /** * Returns an associative array [string => Definition] that maps fully qualified symbol names * to Definitions diff --git a/src/LanguageServer.php b/src/LanguageServer.php index b0af2f0..7e856b1 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -100,6 +100,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ protected $globalIndex; + /** + * @var ProjectIndex + */ + protected $projectIndex; + /** * @var DefinitionResolver */ @@ -182,21 +187,22 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex = new DependenciesIndex; $sourceIndex = new Index; - $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); $stubsIndex = StubsIndex::read(); - $this->globalIndex = new GlobalIndex($stubsIndex, $projectIndex); + $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); $this->documentLoader = new PhpDocumentLoader( $this->contentRetriever, - $projectIndex, + $this->projectIndex, $this->definitionResolver ); if ($rootPath !== null) { - yield $this->index($rootPath); + yield $this->beforeIndex($rootPath); + $this->index($rootPath)->otherwise('\\LanguageServer\\crash'); } // Find composer.json @@ -225,7 +231,13 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher ); } if ($this->workspace === null) { - $this->workspace = new Server\Workspace($projectIndex, $dependenciesIndex, $sourceIndex, $this->composerLock, $this->documentLoader); + $this->workspace = new Server\Workspace( + $this->projectIndex, + $dependenciesIndex, + $sourceIndex, + $this->composerLock, + $this->documentLoader + ); } $serverCapabilities = new ServerCapabilities(); @@ -278,6 +290,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher exit(0); } + /** + * Called before indexing, can return a Promise + * + * @param string $rootPath + */ + protected function beforeIndex(string $rootPath) + { + } + /** * Will read and parse the passed source files in the project and add them to the appropiate indexes * @@ -295,8 +316,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $startTime = microtime(true); - foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run) { - $this->client->window->logMessage(MessageType::INFO, $run); + foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run => $text) { + $this->client->window->logMessage(MessageType::INFO, $text); foreach ($uris as $i => $uri) { if ($this->documentLoader->isOpen($uri)) { continue; @@ -325,6 +346,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher ); } } + if ($run === 0) { + $this->projectIndex->setStaticComplete(); + } else { + $this->projectIndex->setComplete(); + } $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage( diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index a3183dc..532d642 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -30,6 +30,7 @@ use LanguageServer\Index\ReadableIndex; use Sabre\Event\Promise; use Sabre\Uri; use function Sabre\Event\coroutine; +use function LanguageServer\waitForEvent; /** * Provides method handlers for all textDocument/* methods @@ -226,6 +227,10 @@ class TextDocument } else { // Definition with a global FQN $fqn = DefinitionResolver::getDefinedFqn($node); + // Wait until indexing finished + if (!$this->index->isComplete()) { + yield waitForEvent($this->index, 'complete'); + } if ($fqn === null) { $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); if ($fqn === null) { @@ -267,11 +272,18 @@ class TextDocument } // Handle definition nodes $fqn = DefinitionResolver::getDefinedFqn($node); - if ($fqn !== null) { - $def = $this->index->getDefinition($fqn); - } else { - // Handle reference nodes - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + while (true) { + if ($fqn) { + $def = $this->index->getDefinition($fqn); + } else { + // Handle reference nodes + $def = $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 @@ -300,14 +312,22 @@ class TextDocument if ($node === null) { return new Hover([]); } - $range = Range::fromNode($node); - if ($definedFqn = DefinitionResolver::getDefinedFqn($node)) { - // Support hover for definitions - $def = $this->index->getDefinition($definedFqn); - } else { - // Get the definition for whatever node is under the cursor - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + $definedFqn = DefinitionResolver::getDefinedFqn($node); + while (true) { + if ($definedFqn) { + // Support hover for definitions + $def = $this->index->getDefinition($definedFqn); + } else { + // Get the definition for whatever node is under the cursor + $def = $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'); } + $range = Range::fromNode($node); if ($def === null) { return new Hover([], $range); } @@ -364,12 +384,18 @@ class TextDocument return []; } // Handle definition nodes - $fqn = DefinitionResolver::getDefinedFqn($node); - if ($fqn !== null) { - $def = $this->index->getDefinition($fqn); - } else { - // Handle reference nodes - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + while (true) { + if ($fqn) { + $def = $this->index->getDefinition($definedFqn); + } else { + // Handle reference nodes + $def = $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 diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 112e601..5aae7cf 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -8,6 +8,7 @@ use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location}; use Sabre\Event\Promise; use function Sabre\Event\coroutine; +use function LanguageServer\waitForEvent; /** * Provides method handlers for all workspace/* methods @@ -61,17 +62,23 @@ 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 SymbolInformation[] + * @return Promise */ - public function symbol(string $query): array + public function symbol(string $query): Promise { - $symbols = []; - foreach ($this->index->getDefinitions() as $fqn => $definition) { - if ($query === '' || stripos($fqn, $query) !== false) { - $symbols[] = $definition->symbolInformation; + return coroutine(function () use ($query) { + // Wait until indexing for definitions finished + if (!$this->index->isStaticComplete()) { + yield waitForEvent($this->index, 'static-complete'); } - } - return $symbols; + $symbols = []; + foreach ($this->index->getDefinitions() as $fqn => $definition) { + if ($query === '' || stripos($fqn, $query) !== false) { + $symbols[] = $definition->symbolInformation; + } + } + return $symbols; + }); } /** @@ -87,6 +94,10 @@ class Workspace if ($this->composerLock === null) { return []; } + // Wait until indexing finished + if (!$this->index->isComplete()) { + yield waitForEvent($this->index, 'complete'); + } /** Map from URI to array of referenced FQNs in dependencies */ $refs = []; // Get all references TO dependencies diff --git a/src/utils.php b/src/utils.php index ed7a419..8ab7beb 100644 --- a/src/utils.php +++ b/src/utils.php @@ -6,7 +6,7 @@ namespace LanguageServer; use Throwable; use InvalidArgumentException; use PhpParser\Node; -use Sabre\Event\{Loop, Promise}; +use Sabre\Event\{Loop, Promise, EmitterInterface}; /** * Transforms an absolute file path into a URI as used by the language server protocol. @@ -79,6 +79,20 @@ function timeout($seconds = 0): Promise return $promise; } +/** + * 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 waitForEvent(EmitterInterface $emitter, string $event): Promise +{ + $p = new Promise; + $emitter->once($event, [$p, 'fulfill']); + return $p; +} + /** * Returns the closest node of a specific type * diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index e37844a..679191f 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -48,6 +48,7 @@ abstract class ServerTestCase extends TestCase $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $projectIndex->setComplete(); $definitionResolver = new DefinitionResolver($projectIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index 4c09e18..f3c0771 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -16,6 +16,7 @@ class GlobalFallbackTest extends ServerTestCase public function setUp() { $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $projectIndex->setComplete(); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $definitionResolver = new DefinitionResolver($projectIndex); $contentRetriever = new FileSystemContentRetriever; diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 2679d15..ac7b355 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -16,6 +16,7 @@ class GlobalFallbackTest extends ServerTestCase public function setUp() { $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $projectIndex->setComplete(); $definitionResolver = new DefinitionResolver($projectIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index b88735c..d3d13b6 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -25,7 +25,7 @@ class SymbolTest extends ServerTestCase public function testEmptyQueryReturnsAllSymbols() { // Request symbols - $result = $this->workspace->symbol(''); + $result = $this->workspace->symbol('')->wait(); $referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php')); // @codingStandardsIgnoreStart $this->assertEquals([ @@ -65,7 +65,7 @@ class SymbolTest extends ServerTestCase public function testQueryFiltersResults() { // Request symbols - $result = $this->workspace->symbol('testmethod'); + $result = $this->workspace->symbol('testmethod')->wait(); // @codingStandardsIgnoreStart $this->assertEquals([ new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),