2016-09-30 09:30:08 +00:00
|
|
|
<?php
|
2016-09-30 09:54:49 +00:00
|
|
|
declare(strict_types = 1);
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
2016-11-14 09:25:44 +00:00
|
|
|
use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities};
|
2016-10-19 10:31:32 +00:00
|
|
|
use phpDocumentor\Reflection\DocBlockFactory;
|
2016-11-14 09:25:44 +00:00
|
|
|
use Sabre\Event\Promise;
|
|
|
|
use function Sabre\Event\coroutine;
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
class Project
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* An associative array [string => PhpDocument]
|
|
|
|
* that maps URIs to loaded PhpDocuments
|
|
|
|
*
|
2016-10-08 10:51:55 +00:00
|
|
|
* @var PhpDocument[]
|
2016-09-30 09:30:08 +00:00
|
|
|
*/
|
2016-10-09 08:09:09 +00:00
|
|
|
private $documents = [];
|
2016-09-30 09:30:08 +00:00
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* An associative array that maps fully qualified symbol names to Definitions
|
2016-10-08 12:59:08 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @var Definition[]
|
2016-10-08 12:59:08 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
private $definitions = [];
|
2016-10-08 12:59:08 +00:00
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
/**
|
|
|
|
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
|
|
|
|
*
|
|
|
|
* @var PhpDocument[][]
|
|
|
|
*/
|
|
|
|
private $references = [];
|
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
/**
|
|
|
|
* Instance of the PHP parser
|
|
|
|
*
|
2016-10-26 20:25:24 +00:00
|
|
|
* @var Parser
|
2016-09-30 09:30:08 +00:00
|
|
|
*/
|
|
|
|
private $parser;
|
|
|
|
|
2016-10-19 10:31:32 +00:00
|
|
|
/**
|
|
|
|
* The DocBlockFactory instance to parse docblocks
|
|
|
|
*
|
|
|
|
* @var DocBlockFactory
|
|
|
|
*/
|
|
|
|
private $docBlockFactory;
|
|
|
|
|
2016-11-18 14:22:24 +00:00
|
|
|
/**
|
|
|
|
* The DefinitionResolver instance to resolve reference nodes to Definitions
|
|
|
|
*
|
|
|
|
* @var DefinitionResolver
|
|
|
|
*/
|
|
|
|
private $definitionResolver;
|
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
/**
|
|
|
|
* Reference to the language server client interface
|
|
|
|
*
|
|
|
|
* @var LanguageClient
|
|
|
|
*/
|
|
|
|
private $client;
|
|
|
|
|
2016-11-14 09:25:44 +00:00
|
|
|
/**
|
|
|
|
* The client's capabilities
|
|
|
|
*
|
|
|
|
* @var ClientCapabilities
|
|
|
|
*/
|
|
|
|
private $clientCapabilities;
|
|
|
|
|
|
|
|
public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities)
|
2016-09-30 09:30:08 +00:00
|
|
|
{
|
|
|
|
$this->client = $client;
|
2016-11-14 09:25:44 +00:00
|
|
|
$this->clientCapabilities = $clientCapabilities;
|
2016-10-26 20:25:24 +00:00
|
|
|
$this->parser = new Parser;
|
2016-10-19 10:31:32 +00:00
|
|
|
$this->docBlockFactory = DocBlockFactory::createInstance();
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->definitionResolver = new DefinitionResolver($this);
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-10-11 12:42:56 +00:00
|
|
|
* Returns the document indicated by uri.
|
2016-11-14 09:25:44 +00:00
|
|
|
* Returns null if the document if not loaded.
|
2016-09-30 09:30:08 +00:00
|
|
|
*
|
|
|
|
* @param string $uri
|
2016-11-14 09:25:44 +00:00
|
|
|
* @return PhpDocument|null
|
2016-09-30 09:30:08 +00:00
|
|
|
*/
|
|
|
|
public function getDocument(string $uri)
|
|
|
|
{
|
2016-11-14 09:25:44 +00:00
|
|
|
return $this->documents[$uri] ?? null;
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
2016-11-14 09:25:44 +00:00
|
|
|
* Returns the document indicated by uri.
|
|
|
|
* If the document is not open, loads it.
|
|
|
|
*
|
|
|
|
* @param string $uri
|
|
|
|
* @return Promise <PhpDocument>
|
|
|
|
*/
|
|
|
|
public function getOrLoadDocument(string $uri)
|
|
|
|
{
|
|
|
|
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads the document by doing a textDocument/xcontent request to the client.
|
|
|
|
* If the client does not support textDocument/xcontent, tries to read the file from the file system.
|
2016-10-11 12:42:56 +00:00
|
|
|
* The document is NOT added to the list of open documents, but definitions are registered.
|
|
|
|
*
|
|
|
|
* @param string $uri
|
2016-11-14 09:25:44 +00:00
|
|
|
* @return Promise <PhpDocument>
|
2016-10-11 12:42:56 +00:00
|
|
|
*/
|
2016-11-14 09:25:44 +00:00
|
|
|
public function loadDocument(string $uri): Promise
|
2016-10-11 12:42:56 +00:00
|
|
|
{
|
2016-11-14 09:25:44 +00:00
|
|
|
return coroutine(function () use ($uri) {
|
2016-11-18 12:24:26 +00:00
|
|
|
$limit = 150000;
|
2016-11-14 09:25:44 +00:00
|
|
|
if ($this->clientCapabilities->xcontentProvider) {
|
|
|
|
$content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text;
|
2016-11-18 12:24:26 +00:00
|
|
|
$size = strlen($content);
|
|
|
|
if ($size > $limit) {
|
|
|
|
throw new ContentTooLargeException($uri, $size, $limit);
|
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
} else {
|
2016-11-18 12:24:26 +00:00
|
|
|
$path = uriToPath($uri);
|
|
|
|
$size = filesize($path);
|
|
|
|
if ($size > $limit) {
|
|
|
|
throw new ContentTooLargeException($uri, $size, $limit);
|
|
|
|
}
|
|
|
|
$content = file_get_contents($path);
|
2016-11-14 09:25:44 +00:00
|
|
|
}
|
|
|
|
if (isset($this->documents[$uri])) {
|
|
|
|
$document = $this->documents[$uri];
|
|
|
|
$document->updateContent($content);
|
|
|
|
} else {
|
2016-11-18 14:22:24 +00:00
|
|
|
$document = new PhpDocument(
|
|
|
|
$uri,
|
|
|
|
$content,
|
|
|
|
$this,
|
|
|
|
$this->client,
|
|
|
|
$this->parser,
|
|
|
|
$this->docBlockFactory,
|
|
|
|
$this->definitionResolver
|
|
|
|
);
|
2016-11-14 09:25:44 +00:00
|
|
|
}
|
|
|
|
return $document;
|
|
|
|
});
|
2016-10-11 12:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensures a document is loaded and added to the list of open documents.
|
|
|
|
*
|
|
|
|
* @param string $uri
|
|
|
|
* @param string $content
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function openDocument(string $uri, string $content)
|
|
|
|
{
|
|
|
|
if (isset($this->documents[$uri])) {
|
|
|
|
$document = $this->documents[$uri];
|
|
|
|
$document->updateContent($content);
|
|
|
|
} else {
|
2016-11-18 14:22:24 +00:00
|
|
|
$document = new PhpDocument(
|
|
|
|
$uri,
|
|
|
|
$content,
|
|
|
|
$this,
|
|
|
|
$this->client,
|
|
|
|
$this->parser,
|
|
|
|
$this->docBlockFactory,
|
|
|
|
$this->definitionResolver
|
|
|
|
);
|
2016-10-11 12:42:56 +00:00
|
|
|
$this->documents[$uri] = $document;
|
|
|
|
}
|
|
|
|
return $document;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the document with the specified URI from the list of open documents
|
|
|
|
*
|
|
|
|
* @param string $uri
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function closeDocument(string $uri)
|
|
|
|
{
|
|
|
|
unset($this->documents[$uri]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the document is open (and loaded)
|
|
|
|
*
|
|
|
|
* @param string $uri
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isDocumentOpen(string $uri): bool
|
|
|
|
{
|
|
|
|
return isset($this->documents[$uri]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
|
|
|
* to Definitions
|
|
|
|
*
|
|
|
|
* @return Definitions[]
|
|
|
|
*/
|
|
|
|
public function getDefinitions()
|
|
|
|
{
|
|
|
|
return $this->definitions;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the Definition object by a specific FQN
|
2016-10-11 12:42:56 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @param string $fqn
|
|
|
|
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
|
|
|
|
* @return Definition|null
|
2016-10-11 12:42:56 +00:00
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function getDefinition(string $fqn, $globalFallback = false)
|
2016-10-11 12:42:56 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
if (isset($this->definitions[$fqn])) {
|
|
|
|
return $this->definitions[$fqn];
|
|
|
|
} else if ($globalFallback) {
|
|
|
|
$parts = explode('\\', $fqn);
|
|
|
|
$fqn = end($parts);
|
|
|
|
return $this->getDefinition($fqn);
|
|
|
|
}
|
2016-10-11 12:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Registers a definition
|
2016-10-08 12:59:08 +00:00
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
2016-11-18 14:22:24 +00:00
|
|
|
* @param string $definition The Definition object
|
2016-10-08 12:59:08 +00:00
|
|
|
* @return void
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function setDefinition(string $fqn, Definition $definition)
|
2016-10-08 12:59:08 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->definitions[$fqn] = $definition;
|
2016-10-08 12:59:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-20 01:48:30 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Sets the Definition index
|
2016-10-20 01:48:30 +00:00
|
|
|
*
|
2016-11-18 14:22:24 +00:00
|
|
|
* @param Definition[] $definitions Map from FQN to Definition
|
2016-10-20 01:48:30 +00:00
|
|
|
* @return void
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function setDefinitions(array $definitions)
|
2016-10-20 01:48:30 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
$this->definitions = $definitions;
|
2016-10-20 01:48:30 +00:00
|
|
|
}
|
|
|
|
|
2016-10-19 11:33:43 +00:00
|
|
|
/**
|
2016-11-18 14:22:24 +00:00
|
|
|
* Unsets the Definition for a specific symbol
|
2016-10-19 11:33:43 +00:00
|
|
|
* and removes all references pointing to that symbol
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
|
|
|
* @return void
|
|
|
|
*/
|
2016-11-18 14:22:24 +00:00
|
|
|
public function removeDefinition(string $fqn)
|
2016-10-24 17:35:37 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
unset($this->definitions[$fqn]);
|
2016-10-19 11:33:43 +00:00
|
|
|
unset($this->references[$fqn]);
|
|
|
|
}
|
|
|
|
|
2016-10-11 23:45:15 +00:00
|
|
|
/**
|
|
|
|
* Adds a document URI as a referencee of a specific symbol
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function addReferenceUri(string $fqn, string $uri)
|
|
|
|
{
|
|
|
|
if (!isset($this->references[$fqn])) {
|
|
|
|
$this->references[$fqn] = [];
|
|
|
|
}
|
|
|
|
// TODO: use DS\Set instead of searching array
|
|
|
|
if (array_search($uri, $this->references[$fqn], true) === false) {
|
|
|
|
$this->references[$fqn][] = $uri;
|
|
|
|
}
|
|
|
|
}
|
2016-10-19 11:33:43 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes a document URI as the container for a specific symbol
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
|
|
|
* @param string $uri The URI
|
|
|
|
* @return void
|
|
|
|
*/
|
2016-10-24 17:35:37 +00:00
|
|
|
public function removeReferenceUri(string $fqn, string $uri)
|
|
|
|
{
|
2016-10-19 11:33:43 +00:00
|
|
|
if (!isset($this->references[$fqn])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$index = array_search($fqn, $this->references[$fqn], true);
|
|
|
|
if ($index === false) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
array_splice($this->references[$fqn], $index, 1);
|
|
|
|
}
|
2016-10-11 23:45:15 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all documents that reference a symbol
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
2016-11-14 09:25:44 +00:00
|
|
|
* @return Promise <PhpDocument[]>
|
2016-10-11 23:45:15 +00:00
|
|
|
*/
|
2016-11-14 09:25:44 +00:00
|
|
|
public function getReferenceDocuments(string $fqn): Promise
|
2016-10-11 23:45:15 +00:00
|
|
|
{
|
|
|
|
if (!isset($this->references[$fqn])) {
|
2016-11-14 09:25:44 +00:00
|
|
|
return Promise\resolve([]);
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn]));
|
2016-10-11 23:45:15 +00:00
|
|
|
}
|
|
|
|
|
2016-10-20 01:48:30 +00:00
|
|
|
/**
|
|
|
|
* Returns an associative array [string => string[]] that maps fully qualified symbol names
|
|
|
|
* to URIs of the document where the symbol is referenced
|
|
|
|
*
|
|
|
|
* @return string[][]
|
|
|
|
*/
|
|
|
|
public function getReferenceUris()
|
|
|
|
{
|
|
|
|
return $this->references;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the reference index
|
|
|
|
*
|
|
|
|
* @param string[][] $references an associative array [string => string[]] from FQN to URIs
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function setReferenceUris(array $references)
|
|
|
|
{
|
|
|
|
$this->references = $references;
|
|
|
|
}
|
|
|
|
|
2016-10-08 12:59:08 +00:00
|
|
|
/**
|
|
|
|
* Returns the document where a symbol is defined
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
2016-11-14 09:25:44 +00:00
|
|
|
* @return Promise <PhpDocument|null>
|
2016-10-08 12:59:08 +00:00
|
|
|
*/
|
2016-11-14 09:25:44 +00:00
|
|
|
public function getDefinitionDocument(string $fqn): Promise
|
2016-10-08 12:59:08 +00:00
|
|
|
{
|
2016-11-18 14:22:24 +00:00
|
|
|
if (!isset($this->definitions[$fqn])) {
|
2016-11-14 09:25:44 +00:00
|
|
|
return Promise\resolve(null);
|
|
|
|
}
|
2016-11-18 14:22:24 +00:00
|
|
|
return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri);
|
2016-10-08 12:59:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-09 08:09:09 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the given FQN is defined in the project
|
|
|
|
*
|
|
|
|
* @param string $fqn The fully qualified name of the symbol
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isDefined(string $fqn): bool
|
|
|
|
{
|
|
|
|
return isset($this->definitions[$fqn]);
|
|
|
|
}
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|