1
0
Fork 0

Merge branch 'master' into signatureHelp

pull/250/head
Ivan Bozhanov 2017-01-26 19:05:09 +02:00 committed by GitHub
commit d4d0c47cdd
17 changed files with 292 additions and 39 deletions

View File

@ -2,6 +2,7 @@
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter}; use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
use Sabre\Event\Loop; use Sabre\Event\Loop;
use Composer\{Factory, XdebugHandler};
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']); $options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
@ -30,6 +31,9 @@ set_exception_handler(function (\Throwable $e) {
@cli_set_process_title('PHP Language Server'); @cli_set_process_title('PHP Language Server');
// If XDebug is enabled, restart without it
(new XdebugHandler(Factory::createOutput()))->check();
if (!empty($options['tcp'])) { if (!empty($options['tcp'])) {
// Connect to a TCP server // Connect to a TCP server
$address = $options['tcp']; $address = $options['tcp'];

View File

@ -36,7 +36,8 @@
"webmozart/path-util": "^2.3", "webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1", "webmozart/glob": "^4.1",
"sabre/uri": "^2.0", "sabre/uri": "^2.0",
"JetBrains/phpstorm-stubs": "dev-master" "JetBrains/phpstorm-stubs": "dev-master",
"composer/composer": "^1.3"
}, },
"repositories": [ "repositories": [
{ {

View File

@ -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();

View File

@ -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,87 @@ abstract class AbstractAggregateIndex implements ReadableIndex
*/ */
abstract protected function getIndexes(): array; 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 * Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions * to Definitions

View File

@ -27,7 +27,9 @@ class DependenciesIndex extends AbstractAggregateIndex
public function getDependencyIndex(string $packageName): Index public function getDependencyIndex(string $packageName): Index
{ {
if (!isset($this->indexes[$packageName])) { if (!isset($this->indexes[$packageName])) {
$this->indexes[$packageName] = new Index; $index = new Index;
$this->indexes[$packageName] = $index;
$this->registerIndex($index);
} }
return $this->indexes[$packageName]; return $this->indexes[$packageName];
} }

View File

@ -26,6 +26,7 @@ class GlobalIndex extends AbstractAggregateIndex
{ {
$this->stubsIndex = $stubsIndex; $this->stubsIndex = $stubsIndex;
$this->projectIndex = $projectIndex; $this->projectIndex = $projectIndex;
parent::__construct();
} }
/** /**

View File

@ -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,61 @@ class Index implements ReadableIndex
*/ */
private $references = []; 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 * Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions * to Definitions
@ -65,6 +123,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');
} }
/** /**

View File

@ -26,6 +26,7 @@ class ProjectIndex extends AbstractAggregateIndex
{ {
$this->sourceIndex = $sourceIndex; $this->sourceIndex = $sourceIndex;
$this->dependenciesIndex = $dependenciesIndex; $this->dependenciesIndex = $dependenciesIndex;
parent::__construct();
} }
/** /**

View File

@ -4,12 +4,31 @@ 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 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 * Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions * to Definitions

View File

@ -101,6 +101,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
protected $globalIndex; protected $globalIndex;
/**
* @var ProjectIndex
*/
protected $projectIndex;
/** /**
* @var DefinitionResolver * @var DefinitionResolver
*/ */
@ -183,21 +188,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
@ -226,7 +232,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();
@ -282,6 +294,15 @@ 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
* *
@ -299,8 +320,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$startTime = microtime(true); $startTime = microtime(true);
foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run) { foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run => $text) {
$this->client->window->logMessage(MessageType::INFO, $run); $this->client->window->logMessage(MessageType::INFO, $text);
foreach ($uris as $i => $uri) { foreach ($uris as $i => $uri) {
if ($this->documentLoader->isOpen($uri)) { if ($this->documentLoader->isOpen($uri)) {
continue; continue;
@ -329,6 +350,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
); );
} }
} }
if ($run === 0) {
$this->projectIndex->setStaticComplete();
} else {
$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(

View File

@ -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
@ -232,6 +233,10 @@ class TextDocument
} else { } else {
// Definition with a global FQN // Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = DefinitionResolver::getDefinedFqn($node);
// Wait until indexing finished
if (!$this->index->isComplete()) {
yield waitForEvent($this->index, 'complete');
}
if ($fqn === null) { if ($fqn === null) {
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn === null) { if ($fqn === null) {
@ -273,12 +278,19 @@ class TextDocument
} }
// Handle definition nodes // Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn !== null) { while (true) {
if ($fqn) {
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
} 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
|| $def->symbolInformation === null || $def->symbolInformation === null
@ -306,14 +318,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) {
if ($definedFqn) {
// Support hover for definitions // Support hover for definitions
$def = $this->index->getDefinition($definedFqn); $def = $this->index->getDefinition($definedFqn);
} else { } else {
// Get the definition for whatever node is under the cursor // Get the definition for whatever node is under the cursor
$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');
}
$range = Range::fromNode($node);
if ($def === null) { if ($def === null) {
return new Hover([], $range); return new Hover([], $range);
} }
@ -378,13 +398,19 @@ 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
|| $def->symbolInformation === null || $def->symbolInformation === null

View File

@ -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,10 +62,15 @@ 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
{ {
return coroutine(function () use ($query) {
// Wait until indexing for definitions finished
if (!$this->index->isStaticComplete()) {
yield waitForEvent($this->index, 'static-complete');
}
$symbols = []; $symbols = [];
foreach ($this->index->getDefinitions() as $fqn => $definition) { foreach ($this->index->getDefinitions() as $fqn => $definition) {
if ($query === '' || stripos($fqn, $query) !== false) { if ($query === '' || stripos($fqn, $query) !== false) {
@ -72,6 +78,7 @@ class Workspace
} }
} }
return $symbols; 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

View File

@ -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
* *

View File

@ -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);

View File

@ -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;

View File

@ -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();
$definitionResolver = new DefinitionResolver($projectIndex); $definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);

View File

@ -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'),