parent
49245fd4d3
commit
106aa24b5d
|
@ -42,8 +42,8 @@ class ProjectIndex extends AbstractAggregateIndex
|
||||||
*/
|
*/
|
||||||
public function getIndexForUri(string $uri): Index
|
public function getIndexForUri(string $uri): Index
|
||||||
{
|
{
|
||||||
if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) {
|
if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) {
|
||||||
$packageName = $matches[0];
|
$packageName = $matches[1];
|
||||||
return $this->dependenciesIndex->getDependencyIndex($packageName);
|
return $this->dependenciesIndex->getDependencyIndex($packageName);
|
||||||
}
|
}
|
||||||
return $this->sourceIndex;
|
return $this->sourceIndex;
|
||||||
|
|
|
@ -81,6 +81,30 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
*/
|
*/
|
||||||
protected $documentLoader;
|
protected $documentLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parsed composer.json file in the project, if any
|
||||||
|
*
|
||||||
|
* @var \stdClass
|
||||||
|
*/
|
||||||
|
protected $composerJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parsed composer.lock file in the project, if any
|
||||||
|
*
|
||||||
|
* @var \stdClass
|
||||||
|
*/
|
||||||
|
protected $composerLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var GlobalIndex
|
||||||
|
*/
|
||||||
|
protected $globalIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DefinitionResolver
|
||||||
|
*/
|
||||||
|
protected $definitionResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param PotocolReader $reader
|
* @param PotocolReader $reader
|
||||||
* @param ProtocolWriter $writer
|
* @param ProtocolWriter $writer
|
||||||
|
@ -144,8 +168,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
||||||
|
|
||||||
yield null;
|
|
||||||
|
|
||||||
if ($capabilities->xfilesProvider) {
|
if ($capabilities->xfilesProvider) {
|
||||||
$this->filesFinder = new ClientFilesFinder($this->client);
|
$this->filesFinder = new ClientFilesFinder($this->client);
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,31 +180,53 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$this->contentRetriever = new FileSystemContentRetriever;
|
$this->contentRetriever = new FileSystemContentRetriever;
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
|
$dependenciesIndex = new DependenciesIndex;
|
||||||
|
$sourceIndex = new Index;
|
||||||
|
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||||
$stubsIndex = StubsIndex::read();
|
$stubsIndex = StubsIndex::read();
|
||||||
$globalIndex = new GlobalIndex($stubsIndex, $projectIndex);
|
$this->globalIndex = new GlobalIndex($stubsIndex, $projectIndex);
|
||||||
|
|
||||||
// The DefinitionResolver should look in stubs, the project source and dependencies
|
// The DefinitionResolver should look in stubs, the project source and dependencies
|
||||||
$definitionResolver = new DefinitionResolver($globalIndex);
|
$this->definitionResolver = new DefinitionResolver($this->globalIndex);
|
||||||
|
|
||||||
$this->documentLoader = new PhpDocumentLoader(
|
$this->documentLoader = new PhpDocumentLoader(
|
||||||
$this->contentRetriever,
|
$this->contentRetriever,
|
||||||
$projectIndex,
|
$projectIndex,
|
||||||
$definitionResolver
|
$this->definitionResolver
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($rootPath !== null) {
|
if ($rootPath !== null) {
|
||||||
$this->index($rootPath)->otherwise('\\LanguageServer\\crash');
|
yield $this->index($rootPath)->otherwise('LanguageServer\\crash');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->textDocument = new Server\TextDocument(
|
// Find composer.json
|
||||||
$this->documentLoader,
|
if ($this->composerJson === null) {
|
||||||
$definitionResolver,
|
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
|
||||||
$this->client,
|
if (!empty($composerJsonFiles)) {
|
||||||
$globalIndex
|
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
|
||||||
);
|
}
|
||||||
// workspace/symbol should only look inside the project source and dependencies
|
}
|
||||||
$this->workspace = new Server\Workspace($projectIndex, $this->client);
|
// Find composer.lock
|
||||||
|
if ($this->composerLock === null) {
|
||||||
|
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
|
||||||
|
if (!empty($composerLockFiles)) {
|
||||||
|
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->textDocument === null) {
|
||||||
|
$this->textDocument = new Server\TextDocument(
|
||||||
|
$this->documentLoader,
|
||||||
|
$this->definitionResolver,
|
||||||
|
$this->client,
|
||||||
|
$this->globalIndex,
|
||||||
|
$this->composerJson,
|
||||||
|
$this->composerLock
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($this->workspace === null) {
|
||||||
|
$this->workspace = new Server\Workspace($projectIndex, $dependenciesIndex, $sourceIndex, $this->composerLock, $this->documentLoader);
|
||||||
|
}
|
||||||
|
|
||||||
$serverCapabilities = new ServerCapabilities();
|
$serverCapabilities = new ServerCapabilities();
|
||||||
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
||||||
|
@ -203,6 +247,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->completionProvider = new CompletionOptions;
|
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||||
|
// Support global references
|
||||||
|
$serverCapabilities->xworkspaceReferencesProvider = true;
|
||||||
|
$serverCapabilities->xdefinitionProvider = true;
|
||||||
|
$serverCapabilities->xdependenciesProvider = true;
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
class DependencyReference
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $hints;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var object
|
||||||
|
*/
|
||||||
|
public $attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param object $attributes
|
||||||
|
* @param mixed $hints
|
||||||
|
*/
|
||||||
|
public function __construct($attributes = null, $hints = null)
|
||||||
|
{
|
||||||
|
$this->attributes = $attributes ?? new \stdClass;
|
||||||
|
$this->hints = $hints;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the symbol that can be used to identify or locate its
|
||||||
|
* definition.
|
||||||
|
*/
|
||||||
|
class ReferenceInformation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The location in the workspace where the `symbol` is referenced.
|
||||||
|
*
|
||||||
|
* @var Location
|
||||||
|
*/
|
||||||
|
public $reference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the symbol that can be used to identify or locate its
|
||||||
|
* definition.
|
||||||
|
*
|
||||||
|
* @var SymbolDescriptor
|
||||||
|
*/
|
||||||
|
public $symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Location $reference The location in the workspace where the `symbol` is referenced.
|
||||||
|
* @param SymbolDescriptor $symbol Metadata about the symbol that can be used to identify or locate its definition.
|
||||||
|
*/
|
||||||
|
public function __construct(Location $reference = null, SymbolDescriptor $symbol = null)
|
||||||
|
{
|
||||||
|
$this->reference = $reference;
|
||||||
|
$this->symbol = $symbol;
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,4 +108,25 @@ class ServerCapabilities
|
||||||
* @var bool|null
|
* @var bool|null
|
||||||
*/
|
*/
|
||||||
public $renameProvider;
|
public $renameProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server provides workspace references exporting support.
|
||||||
|
*
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
public $xworkspaceReferencesProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server provides extended text document definition support.
|
||||||
|
*
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
public $xdefinitionProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server provides workspace dependencies support.
|
||||||
|
*
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
public $dependenciesProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
class SymbolDescriptor extends SymbolInformation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The fully qualified structural element name, a globally unique identifier for the symbol.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $fqsen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A package from the composer.lock file or the contents of the composer.json
|
||||||
|
* Example: https://github.com/composer/composer/blob/master/composer.lock#L10
|
||||||
|
* Available fields may differ
|
||||||
|
*
|
||||||
|
* @var object|null
|
||||||
|
*/
|
||||||
|
public $package;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $fqsen The fully qualified structural element name, a globally unique identifier for the symbol.
|
||||||
|
* @param object $package A package from the composer.lock file or the contents of the composer.json
|
||||||
|
*/
|
||||||
|
public function __construct(string $fqsen = null, $package = null)
|
||||||
|
{
|
||||||
|
$this->fqsen = $fqsen;
|
||||||
|
$this->package = $package;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
class SymbolLocationInformation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The location where the symbol is defined, if any.
|
||||||
|
*
|
||||||
|
* @var Location|null
|
||||||
|
*/
|
||||||
|
public $location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the symbol that can be used to identify or locate its
|
||||||
|
* definition.
|
||||||
|
*
|
||||||
|
* @var SymbolDescriptor
|
||||||
|
*/
|
||||||
|
public $symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SymbolDescriptor $symbol The location where the symbol is defined, if any
|
||||||
|
* @param Location $location Metadata about the symbol that can be used to identify or locate its definition
|
||||||
|
*/
|
||||||
|
public function __construct(SymbolDescriptor $symbol = null, Location $location = null)
|
||||||
|
{
|
||||||
|
$this->symbol = $symbol;
|
||||||
|
$this->location = $location;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ use PhpParser\{Node, NodeTraverser};
|
||||||
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
|
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
|
||||||
use LanguageServer\NodeVisitor\VariableReferencesCollector;
|
use LanguageServer\NodeVisitor\VariableReferencesCollector;
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
|
SymbolLocationInformation,
|
||||||
|
SymbolDescriptor,
|
||||||
TextDocumentItem,
|
TextDocumentItem,
|
||||||
TextDocumentIdentifier,
|
TextDocumentIdentifier,
|
||||||
VersionedTextDocumentIdentifier,
|
VersionedTextDocumentIdentifier,
|
||||||
|
@ -39,44 +41,58 @@ class TextDocument
|
||||||
*
|
*
|
||||||
* @var \LanguageServer\LanguageClient
|
* @var \LanguageServer\LanguageClient
|
||||||
*/
|
*/
|
||||||
private $client;
|
protected $client;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Project
|
* @var Project
|
||||||
*/
|
*/
|
||||||
private $project;
|
protected $project;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var PrettyPrinter
|
* @var PrettyPrinter
|
||||||
*/
|
*/
|
||||||
private $prettyPrinter;
|
protected $prettyPrinter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DefinitionResolver
|
* @var DefinitionResolver
|
||||||
*/
|
*/
|
||||||
private $definitionResolver;
|
protected $definitionResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var CompletionProvider
|
* @var CompletionProvider
|
||||||
*/
|
*/
|
||||||
private $completionProvider;
|
protected $completionProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ReadableIndex
|
* @var ReadableIndex
|
||||||
*/
|
*/
|
||||||
private $index;
|
protected $index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \stdClass|null
|
||||||
|
*/
|
||||||
|
protected $composerJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \stdClass|null
|
||||||
|
*/
|
||||||
|
protected $composerLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param PhpDocumentLoader $documentLoader
|
* @param PhpDocumentLoader $documentLoader
|
||||||
* @param DefinitionResolver $definitionResolver
|
* @param DefinitionResolver $definitionResolver
|
||||||
* @param LanguageClient $client
|
* @param LanguageClient $client
|
||||||
* @param ReadableIndex $index
|
* @param ReadableIndex $index
|
||||||
|
* @param \stdClass $composerJson
|
||||||
|
* @param \stdClass $composerLock
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
PhpDocumentLoader $documentLoader,
|
PhpDocumentLoader $documentLoader,
|
||||||
DefinitionResolver $definitionResolver,
|
DefinitionResolver $definitionResolver,
|
||||||
LanguageClient $client,
|
LanguageClient $client,
|
||||||
ReadableIndex $index
|
ReadableIndex $index,
|
||||||
|
\stdClass $composerJson = null,
|
||||||
|
\stdClass $composerLock = null
|
||||||
) {
|
) {
|
||||||
$this->documentLoader = $documentLoader;
|
$this->documentLoader = $documentLoader;
|
||||||
$this->client = $client;
|
$this->client = $client;
|
||||||
|
@ -84,6 +100,8 @@ class TextDocument
|
||||||
$this->definitionResolver = $definitionResolver;
|
$this->definitionResolver = $definitionResolver;
|
||||||
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
|
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
|
||||||
$this->index = $index;
|
$this->index = $index;
|
||||||
|
$this->composerJson = $composerJson;
|
||||||
|
$this->composerLock = $composerLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -247,7 +265,14 @@ class TextDocument
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
// Handle definition nodes
|
||||||
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
|
if ($fqn !== null) {
|
||||||
|
$def = $this->index->getDefinition($fqn);
|
||||||
|
} else {
|
||||||
|
// Handle reference nodes
|
||||||
|
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
$def === null
|
$def === null
|
||||||
|| $def->symbolInformation === null
|
|| $def->symbolInformation === null
|
||||||
|
@ -317,4 +342,61 @@ class TextDocument
|
||||||
return $this->completionProvider->provideCompletion($document, $position);
|
return $this->completionProvider->provideCompletion($document, $position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is the same as textDocument/definition, except that
|
||||||
|
*
|
||||||
|
* The method returns metadata about the definition (the same metadata that workspace/xreferences searches for).
|
||||||
|
* The concrete location to the definition (location field) is optional. This is useful because the language server
|
||||||
|
* might not be able to resolve a goto definition request to a concrete location (e.g. due to lack of dependencies)
|
||||||
|
* but still may know some information about it.
|
||||||
|
*
|
||||||
|
* @param TextDocumentIdentifier $textDocument The text document
|
||||||
|
* @param Position $position The position inside the text document
|
||||||
|
* @return Promise <SymbolLocationInformation[]>
|
||||||
|
*/
|
||||||
|
public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
|
{
|
||||||
|
return coroutine(function () use ($textDocument, $position) {
|
||||||
|
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
|
$node = $document->getNodeAtPosition($position);
|
||||||
|
if ($node === null) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$def === null
|
||||||
|
|| $def->symbolInformation === null
|
||||||
|
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$symbol = new SymbolDescriptor;
|
||||||
|
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
|
||||||
|
$symbol->$prop = $val;
|
||||||
|
}
|
||||||
|
$symbol->fqsen = $def->fqn;
|
||||||
|
if (preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches) && $this->composerLock !== null) {
|
||||||
|
// Definition is inside a dependency
|
||||||
|
$packageName = $matches[1];
|
||||||
|
foreach ($this->composerLock->packages as $package) {
|
||||||
|
if ($package->name === $packageName) {
|
||||||
|
$symbol->package = $package;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ($this->composerJson !== null) {
|
||||||
|
// Definition belongs to a root package
|
||||||
|
$symbol->package = $this->composerJson;
|
||||||
|
}
|
||||||
|
return [new SymbolLocationInformation($symbol, $symbol->location)];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,17 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use LanguageServer\{LanguageClient, Project};
|
use LanguageServer\{LanguageClient, Project, PhpDocumentLoader};
|
||||||
use LanguageServer\Index\ProjectIndex;
|
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
||||||
use LanguageServer\Protocol\SymbolInformation;
|
use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location};
|
||||||
|
use Sabre\Event\Promise;
|
||||||
|
use function Sabre\Event\coroutine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all workspace/* methods
|
* Provides method handlers for all workspace/* methods
|
||||||
*/
|
*/
|
||||||
class Workspace
|
class Workspace
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The lanugage client object to call methods on the client
|
|
||||||
*
|
|
||||||
* @var \LanguageServer\LanguageClient
|
|
||||||
*/
|
|
||||||
private $client;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The symbol index for the workspace
|
* The symbol index for the workspace
|
||||||
*
|
*
|
||||||
|
@ -27,12 +22,39 @@ class Workspace
|
||||||
private $index;
|
private $index;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ProjectIndex $index Index that is searched on a workspace/symbol request
|
* @var DependenciesIndex
|
||||||
*/
|
*/
|
||||||
public function __construct(ProjectIndex $index, LanguageClient $client)
|
private $dependenciesIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Index
|
||||||
|
*/
|
||||||
|
private $sourceIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \stdClass
|
||||||
|
*/
|
||||||
|
public $composerLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PhpDocumentLoader
|
||||||
|
*/
|
||||||
|
public $documentLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ProjectIndex $index Index that is searched on a workspace/symbol request
|
||||||
|
* @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request
|
||||||
|
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
|
||||||
|
* @param \stdClass $composerLock The parsed composer.lock of the project, if any
|
||||||
|
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
|
||||||
|
*/
|
||||||
|
public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader)
|
||||||
{
|
{
|
||||||
|
$this->sourceIndex = $sourceIndex;
|
||||||
$this->index = $index;
|
$this->index = $index;
|
||||||
$this->client = $client;
|
$this->dependenciesIndex = $dependenciesIndex;
|
||||||
|
$this->composerLock = $composerLock;
|
||||||
|
$this->documentLoader = $documentLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,4 +73,90 @@ class Workspace
|
||||||
}
|
}
|
||||||
return $symbols;
|
return $symbols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workspace references request is sent from the client to the server to locate project-wide references to a symbol given its description / metadata.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* @return ReferenceInformation[]
|
||||||
|
*/
|
||||||
|
public function xreferences($query, array $files = null): Promise
|
||||||
|
{
|
||||||
|
return coroutine(function () use ($query, $files) {
|
||||||
|
if ($this->composerLock === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
/** Map from URI to array of referenced FQNs in dependencies */
|
||||||
|
$refs = [];
|
||||||
|
// Get all references TO dependencies
|
||||||
|
$fqns = isset($query->fqsen) ? [$query->fqsen] : array_values($this->dependenciesIndex->getDefinitions());
|
||||||
|
foreach ($fqns as $fqn) {
|
||||||
|
foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) {
|
||||||
|
if (!isset($refs[$uri])) {
|
||||||
|
$refs[$uri] = [];
|
||||||
|
}
|
||||||
|
if (array_search($uri, $refs[$uri]) === false) {
|
||||||
|
$refs[$uri][] = $fqn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$refInfos = [];
|
||||||
|
foreach ($refs as $uri => $fqns) {
|
||||||
|
foreach ($fqns as $fqn) {
|
||||||
|
$def = $this->dependenciesIndex->getDefinition($fqn);
|
||||||
|
$symbol = new SymbolDescriptor;
|
||||||
|
$symbol->fqsen = $fqn;
|
||||||
|
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
|
||||||
|
$symbol->$prop = $val;
|
||||||
|
}
|
||||||
|
// Find out package name
|
||||||
|
preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $def->symbolInformation->location->uri, $matches);
|
||||||
|
$packageName = $matches[1];
|
||||||
|
foreach ($this->composerLock->packages as $package) {
|
||||||
|
if ($package->name === $packageName) {
|
||||||
|
$symbol->package = $package;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there was no FQSEN provided, check if query attributes match
|
||||||
|
if (!isset($query->fqsen)) {
|
||||||
|
$matches = true;
|
||||||
|
foreach (get_object_vars($query) as $prop => $val) {
|
||||||
|
if ($query->$prop != $symbol->$prop) {
|
||||||
|
$matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$doc = yield $this->documentLoader->getOrLoad($uri);
|
||||||
|
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
|
||||||
|
$refInfo = new ReferenceInformation;
|
||||||
|
$refInfo->reference = Location::fromNode($node);
|
||||||
|
$refInfo->symbol = $symbol;
|
||||||
|
$refInfos[] = $refInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $refInfos;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DependencyReference[]
|
||||||
|
*/
|
||||||
|
public function xdependencies(): array
|
||||||
|
{
|
||||||
|
if ($this->composerLock === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$dependencyReferences = [];
|
||||||
|
foreach ($this->composerLock->packages as $package) {
|
||||||
|
$dependencyReferences[] = new DependencyReference($package);
|
||||||
|
}
|
||||||
|
return $dependencyReferences;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,9 @@ class LanguageServerTest extends TestCase
|
||||||
$serverCapabilities->completionProvider = new CompletionOptions;
|
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||||
|
$serverCapabilities->xworkspaceReferencesProvider = true;
|
||||||
|
$serverCapabilities->xdefinitionProvider = true;
|
||||||
|
$serverCapabilities->xdependenciesProvider = true;
|
||||||
|
|
||||||
$this->assertEquals(new InitializeResult($serverCapabilities), $result);
|
$this->assertEquals(new InitializeResult($serverCapabilities), $result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,13 +45,15 @@ abstract class ServerTestCase extends TestCase
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
|
$sourceIndex = new Index;
|
||||||
|
$dependenciesIndex = new DependenciesIndex;
|
||||||
|
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||||
|
|
||||||
$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);
|
||||||
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
|
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
|
||||||
$this->workspace = new Server\Workspace($projectIndex, $client);
|
$this->workspace = new Server\Workspace($projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader);
|
||||||
|
|
||||||
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
|
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
|
||||||
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
|
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
|
||||||
|
|
Loading…
Reference in New Issue