Make Index an EventEmitter
parent
43a91b0d09
commit
646cce9c21
|
@ -58,6 +58,8 @@ class ComposerScripts
|
||||||
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
|
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$index->setComplete();
|
||||||
|
|
||||||
echo "Saving Index\n";
|
echo "Saving Index\n";
|
||||||
|
|
||||||
$index->save();
|
$index->save();
|
||||||
|
|
|
@ -4,9 +4,12 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\Index;
|
namespace LanguageServer\Index;
|
||||||
|
|
||||||
use LanguageServer\Definition;
|
use LanguageServer\Definition;
|
||||||
|
use Sabre\Event\EmitterTrait;
|
||||||
|
|
||||||
abstract class AbstractAggregateIndex implements ReadableIndex
|
abstract class AbstractAggregateIndex implements ReadableIndex
|
||||||
{
|
{
|
||||||
|
use EmitterTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all indexes managed by the aggregate index
|
* Returns all indexes managed by the aggregate index
|
||||||
*
|
*
|
||||||
|
@ -14,6 +17,48 @@ abstract class AbstractAggregateIndex implements ReadableIndex
|
||||||
*/
|
*/
|
||||||
abstract protected function getIndexes(): array;
|
abstract protected function getIndexes(): array;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
foreach ($this->getIndexes() as $index) {
|
||||||
|
$index->on('complete', function () {
|
||||||
|
if ($this->isComplete()) {
|
||||||
|
$this->emit('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();
|
||||||
|
}
|
||||||
|
$this->emit('complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 an associative array [string => Definition] that maps fully qualified symbol names
|
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||||
* to Definitions
|
* to Definitions
|
||||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\Index;
|
namespace LanguageServer\Index;
|
||||||
|
|
||||||
use LanguageServer\Definition;
|
use LanguageServer\Definition;
|
||||||
|
use Sabre\Event\EmitterTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the index of a project or dependency
|
* Represents the index of a project or dependency
|
||||||
|
@ -11,6 +12,8 @@ use LanguageServer\Definition;
|
||||||
*/
|
*/
|
||||||
class Index implements ReadableIndex
|
class Index implements ReadableIndex
|
||||||
{
|
{
|
||||||
|
use EmitterTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An associative array that maps fully qualified symbol names to Definitions
|
* An associative array that maps fully qualified symbol names to Definitions
|
||||||
*
|
*
|
||||||
|
@ -25,6 +28,32 @@ class Index implements ReadableIndex
|
||||||
*/
|
*/
|
||||||
private $references = [];
|
private $references = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $complete = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks this index as complete
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setComplete()
|
||||||
|
{
|
||||||
|
$this->complete = true;
|
||||||
|
$this->emit('complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this index is complete
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isComplete(): bool
|
||||||
|
{
|
||||||
|
return $this->complete;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||||
* to Definitions
|
* to Definitions
|
||||||
|
@ -65,6 +94,7 @@ class Index implements ReadableIndex
|
||||||
public function setDefinition(string $fqn, Definition $definition)
|
public function setDefinition(string $fqn, Definition $definition)
|
||||||
{
|
{
|
||||||
$this->definitions[$fqn] = $definition;
|
$this->definitions[$fqn] = $definition;
|
||||||
|
$this->emit('definition-added');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,12 +4,23 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\Index;
|
namespace LanguageServer\Index;
|
||||||
|
|
||||||
use LanguageServer\Definition;
|
use LanguageServer\Definition;
|
||||||
|
use Sabre\Event\EmitterInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ReadableIndex interface provides methods to lookup definitions and references
|
* The ReadableIndex interface provides methods to lookup definitions and references
|
||||||
|
*
|
||||||
|
* @event definition-added Emitted when a definition was added
|
||||||
|
* @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 an associative array [string => Definition] that maps fully qualified symbol names
|
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||||
* to Definitions
|
* to Definitions
|
||||||
|
|
|
@ -100,6 +100,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
*/
|
*/
|
||||||
protected $globalIndex;
|
protected $globalIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ProjectIndex
|
||||||
|
*/
|
||||||
|
protected $projectIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DefinitionResolver
|
* @var DefinitionResolver
|
||||||
*/
|
*/
|
||||||
|
@ -182,21 +187,22 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
|
|
||||||
$dependenciesIndex = new DependenciesIndex;
|
$dependenciesIndex = new DependenciesIndex;
|
||||||
$sourceIndex = new Index;
|
$sourceIndex = new Index;
|
||||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||||
$stubsIndex = StubsIndex::read();
|
$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
|
// The DefinitionResolver should look in stubs, the project source and dependencies
|
||||||
$this->definitionResolver = new DefinitionResolver($this->globalIndex);
|
$this->definitionResolver = new DefinitionResolver($this->globalIndex);
|
||||||
|
|
||||||
$this->documentLoader = new PhpDocumentLoader(
|
$this->documentLoader = new PhpDocumentLoader(
|
||||||
$this->contentRetriever,
|
$this->contentRetriever,
|
||||||
$projectIndex,
|
$this->projectIndex,
|
||||||
$this->definitionResolver
|
$this->definitionResolver
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($rootPath !== null) {
|
if ($rootPath !== null) {
|
||||||
yield $this->index($rootPath);
|
yield $this->beforeIndex($rootPath);
|
||||||
|
$this->index($rootPath)->otherwise('\\LanguageServer\\crash');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find composer.json
|
// Find composer.json
|
||||||
|
@ -225,7 +231,13 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($this->workspace === null) {
|
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();
|
$serverCapabilities = new ServerCapabilities();
|
||||||
|
@ -278,6 +290,13 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
exit(0);
|
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
|
* Will read and parse the passed source files in the project and add them to the appropiate indexes
|
||||||
*
|
*
|
||||||
|
@ -325,6 +344,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$this->projectIndex->setComplete();
|
||||||
$duration = (int)(microtime(true) - $startTime);
|
$duration = (int)(microtime(true) - $startTime);
|
||||||
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
||||||
$this->client->window->logMessage(
|
$this->client->window->logMessage(
|
||||||
|
|
|
@ -30,6 +30,7 @@ use LanguageServer\Index\ReadableIndex;
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use Sabre\Uri;
|
use Sabre\Uri;
|
||||||
use function Sabre\Event\coroutine;
|
use function Sabre\Event\coroutine;
|
||||||
|
use function LanguageServer\waitForEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all textDocument/* methods
|
* Provides method handlers for all textDocument/* methods
|
||||||
|
@ -267,11 +268,18 @@ class TextDocument
|
||||||
}
|
}
|
||||||
// Handle definition nodes
|
// Handle definition nodes
|
||||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
if ($fqn !== null) {
|
while (true) {
|
||||||
$def = $this->index->getDefinition($fqn);
|
if ($fqn) {
|
||||||
} else {
|
$def = $this->index->getDefinition($definedFqn);
|
||||||
// Handle reference nodes
|
} else {
|
||||||
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
// 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 (
|
if (
|
||||||
$def === null
|
$def === null
|
||||||
|
@ -300,14 +308,22 @@ class TextDocument
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return new Hover([]);
|
return new Hover([]);
|
||||||
}
|
}
|
||||||
$range = Range::fromNode($node);
|
$definedFqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
if ($definedFqn = DefinitionResolver::getDefinedFqn($node)) {
|
while (true) {
|
||||||
// Support hover for definitions
|
if ($definedFqn) {
|
||||||
$def = $this->index->getDefinition($definedFqn);
|
// Support hover for definitions
|
||||||
} else {
|
$def = $this->index->getDefinition($definedFqn);
|
||||||
// Get the definition for whatever node is under the cursor
|
} else {
|
||||||
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
// 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) {
|
if ($def === null) {
|
||||||
return new Hover([], $range);
|
return new Hover([], $range);
|
||||||
}
|
}
|
||||||
|
@ -364,12 +380,18 @@ class TextDocument
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Handle definition nodes
|
// Handle definition nodes
|
||||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
while (true) {
|
||||||
if ($fqn !== null) {
|
if ($fqn) {
|
||||||
$def = $this->index->getDefinition($fqn);
|
$def = $this->index->getDefinition($definedFqn);
|
||||||
} else {
|
} else {
|
||||||
// Handle reference nodes
|
// Handle reference nodes
|
||||||
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
$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 (
|
if (
|
||||||
$def === null
|
$def === null
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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 function Sabre\Event\coroutine;
|
use function Sabre\Event\coroutine;
|
||||||
|
use function LanguageServer\waitForEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all workspace/* methods
|
* 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.
|
* 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 SymbolInformation[]
|
* @return Promise <SymbolInformation[]>
|
||||||
*/
|
*/
|
||||||
public function symbol(string $query): array
|
public function symbol(string $query): Promise
|
||||||
{
|
{
|
||||||
$symbols = [];
|
return coroutine(function () use ($query) {
|
||||||
foreach ($this->index->getDefinitions() as $fqn => $definition) {
|
// Wait until indexing finished
|
||||||
if ($query === '' || stripos($fqn, $query) !== false) {
|
if (!$this->index->isComplete()) {
|
||||||
$symbols[] = $definition->symbolInformation;
|
yield waitForEvent($this->index, 'complete');
|
||||||
}
|
}
|
||||||
}
|
$symbols = [];
|
||||||
return $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) {
|
if ($this->composerLock === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
// Wait until indexing finished
|
||||||
|
if (!$this->index->isComplete()) {
|
||||||
|
yield waitForEvent($this->index, 'complete');
|
||||||
|
}
|
||||||
/** Map from URI to array of referenced FQNs in dependencies */
|
/** Map from URI to array of referenced FQNs in dependencies */
|
||||||
$refs = [];
|
$refs = [];
|
||||||
// Get all references TO dependencies
|
// Get all references TO dependencies
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace LanguageServer;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use PhpParser\Node;
|
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.
|
* 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;
|
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
|
* Returns the closest node of a specific type
|
||||||
*
|
*
|
||||||
|
|
|
@ -48,6 +48,7 @@ abstract class ServerTestCase extends TestCase
|
||||||
$sourceIndex = new Index;
|
$sourceIndex = new Index;
|
||||||
$dependenciesIndex = new DependenciesIndex;
|
$dependenciesIndex = new DependenciesIndex;
|
||||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||||
|
$projectIndex->setComplete();
|
||||||
|
|
||||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||||
|
|
|
@ -16,6 +16,7 @@ class GlobalFallbackTest extends ServerTestCase
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
|
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
|
||||||
|
$projectIndex->setComplete();
|
||||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||||
$contentRetriever = new FileSystemContentRetriever;
|
$contentRetriever = new FileSystemContentRetriever;
|
||||||
|
|
|
@ -25,7 +25,7 @@ class SymbolTest extends ServerTestCase
|
||||||
public function testEmptyQueryReturnsAllSymbols()
|
public function testEmptyQueryReturnsAllSymbols()
|
||||||
{
|
{
|
||||||
// Request symbols
|
// Request symbols
|
||||||
$result = $this->workspace->symbol('');
|
$result = $this->workspace->symbol('')->wait();
|
||||||
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
|
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
|
||||||
// @codingStandardsIgnoreStart
|
// @codingStandardsIgnoreStart
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
|
@ -65,7 +65,7 @@ class SymbolTest extends ServerTestCase
|
||||||
public function testQueryFiltersResults()
|
public function testQueryFiltersResults()
|
||||||
{
|
{
|
||||||
// Request symbols
|
// Request symbols
|
||||||
$result = $this->workspace->symbol('testmethod');
|
$result = $this->workspace->symbol('testmethod')->wait();
|
||||||
// @codingStandardsIgnoreStart
|
// @codingStandardsIgnoreStart
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),
|
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),
|
||||||
|
|
Loading…
Reference in New Issue