1
0
Fork 0

Add Index classes and stubs (#214)

pull/206/head
Felix Becker 2016-12-13 01:51:02 +01:00 committed by GitHub
parent b9f9871156
commit a7d77d844e
32 changed files with 1109 additions and 695 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
vendor/ vendor/
.phpls/ .phpls/
composer.lock composer.lock
stubs

View File

@ -8,10 +8,11 @@ services:
cache: cache:
directories: directories:
- vendor - $HOME/.composer/cache
install: install:
- composer install - composer install
- composer run-script parse-stubs
script: script:
- vendor/bin/phpcs -n - vendor/bin/phpcs -n

View File

@ -49,6 +49,10 @@ Non-Standard: An empty query will return _all_ symbols found in the workspace.
PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings. PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings.
Errors/Warnings from the `vendor` directory are ignored. Errors/Warnings from the `vendor` directory are ignored.
### Stubs for PHP built-ins
Completion, type resolval etc. will use the standard PHP library and common extensions.
### What is considered a definition? ### What is considered a definition?
Globally searchable definitions are: Globally searchable definitions are:
@ -131,6 +135,11 @@ Simply run
and you will get the latest stable release and all dependencies. and you will get the latest stable release and all dependencies.
Running `composer update` will update the server to the latest non-breaking version. Running `composer update` will update the server to the latest non-breaking version.
After installing the language server and its dependencies,
you must parse the stubs for standard PHP symbols and save the index for fast initialization.
composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs
## Running ## Running
Start the language server with Start the language server with
@ -178,6 +187,9 @@ Clone the repository and run
composer install composer install
to install dependencies. to install dependencies.
Then parse the stubs with
composer run-script parse-stubs
Run the tests with Run the tests with

View File

@ -22,6 +22,9 @@
"refactor" "refactor"
], ],
"bin": ["bin/php-language-server.php"], "bin": ["bin/php-language-server.php"],
"scripts": {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs"
},
"require": { "require": {
"php": ">=7.0", "php": ">=7.0",
"nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", "nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c",
@ -32,8 +35,27 @@
"netresearch/jsonmapper": "^1.0", "netresearch/jsonmapper": "^1.0",
"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"
}, },
"repositories": [
{
"type": "package",
"package": {
"name": "JetBrains/phpstorm-stubs",
"version": "dev-master",
"dist": {
"url": "https://github.com/JetBrains/phpstorm-stubs/archive/master.zip",
"type": "zip"
},
"source": {
"url": "https://github.com/JetBrains/phpstorm-stubs",
"type": "git",
"reference": "master"
}
}
}
],
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
"autoload": { "autoload": {

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use PhpParser\Node; use PhpParser\Node;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextEdit, TextEdit,
Range, Range,
@ -97,13 +98,18 @@ class CompletionProvider
private $project; private $project;
/** /**
* @param DefinitionResolver $definitionResolver * @var ReadableIndex
* @param Project $project
*/ */
public function __construct(DefinitionResolver $definitionResolver, Project $project) private $index;
/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{ {
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
$this->project = $project; $this->index = $index;
} }
/** /**
@ -153,7 +159,7 @@ class CompletionProvider
} }
} }
foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) { foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
$list->items[] = CompletionItem::fromDefinition($def); $list->items[] = CompletionItem::fromDefinition($def);
@ -185,7 +191,9 @@ class CompletionProvider
// Get the definition for the used namespace, class-like, function or constant // Get the definition for the used namespace, class-like, function or constant
// And save it under the alias // And save it under the alias
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name); $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); if ($def = $this->index->getDefinition($fqn)) {
$aliasedDefs[$use->alias] = $def;
}
} }
} else { } else {
// Use statements are always the first statements in a namespace // Use statements are always the first statements in a namespace
@ -206,7 +214,7 @@ class CompletionProvider
// Additionally, suggest global symbols that either // Additionally, suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified // - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified // - start with just the prefix, if the Name node is fully qualified
foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($this->index->getDefinitions() as $fqn => $def) {
if ( if (
$def->isGlobal // exclude methods, properties etc. $def->isGlobal // exclude methods, properties etc.
&& ( && (
@ -326,7 +334,7 @@ class CompletionProvider
} }
if ($level instanceof Node\Expr\Closure) { if ($level instanceof Node\Expr\Closure) {
foreach ($level->uses as $use) { foreach ($level->uses as $use) {
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$use->var] = $use; $vars[$use->var] = $use;
} }
} }

57
src/ComposerScripts.php Normal file
View File

@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\FilesFinder\FileSystemFilesFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\StubsIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
use function Sabre\Event\coroutine;
require_once __DIR__ . '/../vendor/sabre/event/lib/coroutine.php';
require_once __DIR__ . '/../vendor/sabre/event/lib/Loop/functions.php';
require_once __DIR__ . '/../vendor/sabre/event/lib/Promise/functions.php';
require_once __DIR__ . '/../vendor/sabre/uri/lib/functions.php';
require_once __DIR__ . '/utils.php';
class ComposerScripts
{
public static function parseStubs()
{
coroutine(function () {
$index = new StubsIndex;
$finder = new FileSystemFilesFinder;
$contentRetriever = new FileSystemContentRetriever;
$docBlockFactory = DocBlockFactory::createInstance();
$parser = new Parser;
$definitionResolver = new DefinitionResolver($index);
$stubsLocation = Path::canonicalize(__DIR__ . '/../vendor/JetBrains/phpstorm-stubs');
$uris = yield $finder->find("$stubsLocation/**/*.php");
foreach ($uris as $uri) {
echo "Parsing $uri\n";
$content = yield $contentRetriever->retrieve($uri);
// Change URI to phpstubs://
$parts = Uri\parse($uri);
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
$parts['scheme'] = 'phpstubs';
$uri = Uri\build($parts);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
}
echo "Saving Index\n";
$index->save();
echo "Finished\n";
})->wait();
}
}

View File

@ -7,15 +7,14 @@ use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
use Sabre\Event\Promise; use LanguageServer\Index\ReadableIndex;
use function Sabre\Event\coroutine;
class DefinitionResolver class DefinitionResolver
{ {
/** /**
* @var \LanguageServer\Project * @var \LanguageServer\Index
*/ */
private $project; private $index;
/** /**
* @var \phpDocumentor\Reflection\TypeResolver * @var \phpDocumentor\Reflection\TypeResolver
@ -27,9 +26,12 @@ class DefinitionResolver
*/ */
private $prettyPrinter; private $prettyPrinter;
public function __construct(Project $project) /**
* @param ReadableIndex $index
*/
public function __construct(ReadableIndex $index)
{ {
$this->project = $project; $this->index = $index;
$this->typeResolver = new TypeResolver; $this->typeResolver = new TypeResolver;
$this->prettyPrinter = new PrettyPrinter; $this->prettyPrinter = new PrettyPrinter;
} }
@ -147,8 +149,8 @@ class DefinitionResolver
// http://php.net/manual/en/language.namespaces.fallback.php // http://php.net/manual/en/language.namespaces.fallback.php
$parent = $node->getAttribute('parentNode'); $parent = $node->getAttribute('parentNode');
$globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall;
// Return the Definition object from the project index // Return the Definition object from the index index
return $this->project->getDefinition($fqn, $globalFallback); return $this->index->getDefinition($fqn, $globalFallback);
} }
/** /**
@ -403,7 +405,7 @@ class DefinitionResolver
return new Types\Mixed; return new Types\Mixed;
} }
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
$def = $this->project->getDefinition($fqn, true); $def = $this->index->getDefinition($fqn, true);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -414,7 +416,7 @@ class DefinitionResolver
} }
// Resolve constant // Resolve constant
$fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name);
$def = $this->project->getDefinition($fqn, true); $def = $this->index->getDefinition($fqn, true);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -443,7 +445,7 @@ class DefinitionResolver
if ($expr instanceof Node\Expr\MethodCall) { if ($expr instanceof Node\Expr\MethodCall) {
$fqn .= '()'; $fqn .= '()';
} }
$def = $this->project->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def !== null) { if ($def !== null) {
return $def->type; return $def->type;
} }
@ -466,7 +468,7 @@ class DefinitionResolver
if ($expr instanceof Node\Expr\StaticCall) { if ($expr instanceof Node\Expr\StaticCall) {
$fqn .= '()'; $fqn .= '()';
} }
$def = $this->project->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def === null) { if ($def === null) {
return new Types\Mixed; return new Types\Mixed;
} }

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
abstract class AbstractAggregateIndex implements ReadableIndex
{
/**
* Returns all indexes managed by the aggregate index
*
* @return ReadableIndex[]
*/
abstract protected function getIndexes(): array;
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definition[]
*/
public function getDefinitions(): array
{
$defs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getDefinitions() as $fqn => $def) {
$defs[$fqn] = $def;
}
}
return $defs;
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
foreach ($this->getIndexes() as $index) {
if ($def = $index->getDefinition($fqn, $globalFallback)) {
return $def;
}
}
}
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array
{
$refs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getReferenceUris($fqn) as $ref) {
$refs[] = $ref;
}
}
return $refs;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
class DependenciesIndex extends AbstractAggregateIndex
{
/**
* Map from package name to index
*
* @var Index[]
*/
protected $indexes = [];
/**
* @return Index[]
*/
protected function getIndexes(): array
{
return $this->indexes;
}
/**
* @param string $packageName
* @return Index
*/
public function getDependencyIndex(string $packageName): Index
{
if (!isset($this->indexes[$packageName])) {
$this->indexes[$packageName] = new Index;
}
return $this->indexes[$packageName];
}
/**
* @param string $packageName
* @return void
*/
public function removeDependencyIndex(string $packageName)
{
unset($this->indexes[$packageName]);
}
/**
* @param string $packageName
* @return bool
*/
public function hasDependencyIndex(string $packageName): bool
{
return isset($this->indexes[$packageName]);
}
}

38
src/Index/GlobalIndex.php Normal file
View File

@ -0,0 +1,38 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
/**
* Aggregates definitions of the project and stubs
*/
class GlobalIndex extends AbstractAggregateIndex
{
/**
* @var Index
*/
private $stubsIndex;
/**
* @var ProjectIndex
*/
private $projectIndex;
/**
* @param StubsIndex $stubsIndex
* @param ProjectIndex $projectIndex
*/
public function __construct(StubsIndex $stubsIndex, ProjectIndex $projectIndex)
{
$this->stubsIndex = $stubsIndex;
$this->projectIndex = $projectIndex;
}
/**
* @return ReadableIndex[]
*/
protected function getIndexes(): array
{
return [$this->stubsIndex, $this->projectIndex];
}
}

129
src/Index/Index.php Normal file
View File

@ -0,0 +1,129 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
/**
* Represents the index of a project or dependency
* Serializable for caching
*/
class Index implements ReadableIndex
{
/**
* An associative array that maps fully qualified symbol names to Definitions
*
* @var Definition[]
*/
private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
*
* @var string[][]
*/
private $references = [];
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definition[]
*/
public function getDefinitions(): array
{
return $this->definitions;
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
if (isset($this->definitions[$fqn])) {
return $this->definitions[$fqn];
}
if ($globalFallback) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
return $this->getDefinition($fqn);
}
}
/**
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
$this->definitions[$fqn] = $definition;
}
/**
* Unsets the Definition for a specific symbol
* and removes all references pointing to that symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function removeDefinition(string $fqn)
{
unset($this->definitions[$fqn]);
unset($this->references[$fqn]);
}
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array
{
return $this->references[$fqn] ?? [];
}
/**
* 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;
}
}
/**
* 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
*/
public function removeReferenceUri(string $fqn, string $uri)
{
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);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
/**
* A project index manages the source and dependency indexes
*/
class ProjectIndex extends AbstractAggregateIndex
{
/**
* The index for dependencies
*
* @var DependenciesIndex
*/
private $dependenciesIndex;
/**
* The Index for the project source
*
* @var Index
*/
private $sourceIndex;
public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesIndex)
{
$this->sourceIndex = $sourceIndex;
$this->dependenciesIndex = $dependenciesIndex;
}
/**
* @return ReadableIndex[]
*/
protected function getIndexes(): array
{
return [$this->sourceIndex, $this->dependenciesIndex];
}
/**
* @param string $uri
* @return Index
*/
public function getIndexForUri(string $uri): Index
{
if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) {
$packageName = $matches[0];
return $this->dependenciesIndex->getDependencyIndex($packageName);
}
return $this->sourceIndex;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
/**
* The ReadableIndex interface provides methods to lookup definitions and references
*/
interface ReadableIndex
{
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
*
* @return Definitions[]
*/
public function getDefinitions(): array;
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false);
/**
* Returns all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
*/
public function getReferenceUris(string $fqn): array;
}

27
src/Index/StubsIndex.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
class StubsIndex extends Index
{
/**
* Reads the serialized StubsIndex from disk
*
* @return self
*/
public static function read()
{
return unserialize(file_get_contents(__DIR__ . '/../../stubs'));
}
/**
* Serializes and saves the StubsIndex
*
* @return void
*/
public function save()
{
file_put_contents(__DIR__ . '/../../stubs', serialize($this));
}
}

View File

@ -16,12 +16,14 @@ use LanguageServer\Protocol\{
}; };
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise}; use Sabre\Event\{Loop, Promise};
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Exception; use Exception;
use Throwable; use Throwable;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Webmozart\Glob\Glob;
use Sabre\Uri; use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher class LanguageServer extends AdvancedJsonRpc\Dispatcher
@ -40,22 +42,29 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
public $workspace; public $workspace;
public $telemetry; /**
* @var Server\Window
*/
public $window; public $window;
public $telemetry;
public $completionItem; public $completionItem;
public $codeLens; public $codeLens;
/**
* @var ProtocolReader
*/
private $protocolReader; private $protocolReader;
private $protocolWriter;
private $client;
/** /**
* The root project path that was passed to initialize() * @var ProtocolWriter
*
* @var string
*/ */
private $rootPath; private $protocolWriter;
private $project;
/**
* @var LanguageClient
*/
private $client;
/** /**
* @var FilesFinder * @var FilesFinder
@ -65,8 +74,12 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
/** /**
* @var ContentRetriever * @var ContentRetriever
*/ */
private $contentRetrieverFinder; private $contentRetriever;
/**
* @param PotocolReader $reader
* @param ProtocolWriter $writer
*/
public function __construct(ProtocolReader $reader, ProtocolWriter $writer) public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{ {
parent::__construct($this, '/'); parent::__construct($this, '/');
@ -92,7 +105,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} catch (Throwable $e) { } catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response // If an unexpected error occured, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error( $error = new AdvancedJsonRpc\Error(
$e->getMessage(), (string)$e,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null, null,
$e $e
@ -120,54 +133,74 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
* @return InitializeResult * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise
{ {
$this->rootPath = $rootPath; return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {
$this->filesFinder = new ClientFilesFinder($this->client); $this->filesFinder = new ClientFilesFinder($this->client);
} else { } else {
$this->filesFinder = new FileSystemFilesFinder; $this->filesFinder = new FileSystemFilesFinder;
} }
if ($capabilities->xcontentProvider) { if ($capabilities->xcontentProvider) {
$this->contentRetriever = new ClientContentRetriever($this->client); $this->contentRetriever = new ClientContentRetriever($this->client);
} else { } else {
$this->contentRetriever = new FileSystemContentRetriever; $this->contentRetriever = new FileSystemContentRetriever;
} }
$this->project = new Project($this->client, $this->contentRetriever); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->textDocument = new Server\TextDocument($this->project, $this->client); $stubsIndex = StubsIndex::read();
$this->workspace = new Server\Workspace($this->project, $this->client); $globalIndex = new GlobalIndex($stubsIndex, $projectIndex);
// start building project index // The DefinitionResolver should look in stubs, the project source and dependencies
if ($rootPath !== null) { $definitionResolver = new DefinitionResolver($globalIndex);
$this->indexProject()->otherwise('\\LanguageServer\\crash');
}
$serverCapabilities = new ServerCapabilities(); $this->documentLoader = new PhpDocumentLoader(
// Ask the client to return always full documents (because we need to rebuild the AST from scratch) $this->contentRetriever,
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; $projectIndex,
// Support "Find all symbols" $definitionResolver
$serverCapabilities->documentSymbolProvider = true; );
// Support "Find all symbols in workspace"
$serverCapabilities->workspaceSymbolProvider = true;
// Support "Format Code"
$serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition"
$serverCapabilities->definitionProvider = true;
// Support "Find all references"
$serverCapabilities->referencesProvider = true;
// Support "Hover"
$serverCapabilities->hoverProvider = true;
// Support "Completion"
$serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
return new InitializeResult($serverCapabilities); if ($rootPath !== null) {
$pattern = Path::makeAbsolute('**/*.php', $rootPath);
$uris = yield $this->filesFinder->find($pattern);
$this->index($uris)->otherwise('\\LanguageServer\\crash');
}
$this->textDocument = new Server\TextDocument(
$this->documentLoader,
$definitionResolver,
$this->client,
$globalIndex
);
// workspace/symbol should only look inside the project source and dependencies
$this->workspace = new Server\Workspace($projectIndex, $this->client);
$serverCapabilities = new ServerCapabilities();
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
// Support "Find all symbols"
$serverCapabilities->documentSymbolProvider = true;
// Support "Find all symbols in workspace"
$serverCapabilities->workspaceSymbolProvider = true;
// Support "Format Code"
$serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition"
$serverCapabilities->definitionProvider = true;
// Support "Find all references"
$serverCapabilities->referencesProvider = true;
// Support "Hover"
$serverCapabilities->hoverProvider = true;
// Support "Completion"
$serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
return new InitializeResult($serverCapabilities);
});
} }
/** /**
@ -193,20 +226,24 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} }
/** /**
* Parses workspace files, one at a time. * Will read and parse the passed source files in the project and add them to the appropiate indexes
* *
* @return Promise <void> * @return Promise <void>
*/ */
private function indexProject(): Promise private function index(array $uris): Promise
{ {
return coroutine(function () { return coroutine(function () use ($uris) {
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
$uris = yield $this->filesFinder->find($pattern);
$count = count($uris); $count = count($uris);
$startTime = microtime(true); $startTime = microtime(true);
// Parse PHP files
foreach ($uris as $i => $uri) { foreach ($uris as $i => $uri) {
if ($this->documentLoader->isOpen($uri)) {
continue;
}
// Give LS to the chance to handle requests while indexing // Give LS to the chance to handle requests while indexing
yield timeout(); yield timeout();
$this->client->window->logMessage( $this->client->window->logMessage(
@ -214,7 +251,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
"Parsing file $i/$count: {$uri}" "Parsing file $i/$count: {$uri}"
); );
try { try {
yield $this->project->loadDocument($uri); $document = yield $this->documentLoader->load($uri);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
}
} catch (ContentTooLargeException $e) { } catch (ContentTooLargeException $e) {
$this->client->window->logMessage( $this->client->window->logMessage(
MessageType::INFO, MessageType::INFO,

View File

@ -10,34 +10,16 @@ use LanguageServer\NodeVisitor\{
DocBlockParser, DocBlockParser,
DefinitionCollector, DefinitionCollector,
ColumnCalculator, ColumnCalculator,
ReferencesCollector, ReferencesCollector
VariableReferencesCollector
}; };
use LanguageServer\Index\Index;
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Sabre\Uri; use Sabre\Uri;
class PhpDocument class PhpDocument
{ {
/**
* The LanguageClient instance (to report errors etc)
*
* @var LanguageClient
*/
private $client;
/**
* The Project this document belongs to (to register definitions etc)
*
* @var Project
*/
public $project;
// for whatever reason I get "cannot access private property" error if $project is not public
// https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359
/** /**
* The PHPParser instance * The PHPParser instance
* *
@ -59,6 +41,11 @@ class PhpDocument
*/ */
private $definitionResolver; private $definitionResolver;
/**
* @var Index
*/
private $index;
/** /**
* The URI of the document * The URI of the document
* *
@ -102,25 +89,30 @@ class PhpDocument
private $referenceNodes; private $referenceNodes;
/** /**
* @param string $uri The URI of the document * Diagnostics for this document that were collected while parsing
* @param string $content The content of the document *
* @param Project $project The Project this document belongs to (to register definitions etc) * @var Diagnostic[]
* @param LanguageClient $client The LanguageClient instance (to report errors etc) */
* @param Parser $parser The PHPParser instance private $diagnostics;
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
/**
* @param string $uri The URI of the document
* @param string $content The content of the document
* @param Index $index The Index to register definitions and references to
* @param Parser $parser The PHPParser instance
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace
*/ */
public function __construct( public function __construct(
string $uri, string $uri,
string $content, string $content,
Project $project, Index $index,
LanguageClient $client,
Parser $parser, Parser $parser,
DocBlockFactory $docBlockFactory, DocBlockFactory $docBlockFactory,
DefinitionResolver $definitionResolver DefinitionResolver $definitionResolver
) { ) {
$this->uri = $uri; $this->uri = $uri;
$this->project = $project; $this->index = $index;
$this->client = $client;
$this->parser = $parser; $this->parser = $parser;
$this->docBlockFactory = $docBlockFactory; $this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
@ -154,9 +146,9 @@ class PhpDocument
$errorHandler = new ErrorHandler\Collecting; $errorHandler = new ErrorHandler\Collecting;
$stmts = $this->parser->parse($content, $errorHandler); $stmts = $this->parser->parse($content, $errorHandler);
$diagnostics = []; $this->diagnostics = [];
foreach ($errorHandler->getErrors() as $error) { foreach ($errorHandler->getErrors() as $error) {
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
} }
// $stmts can be null in case of a fatal parsing error // $stmts can be null in case of a fatal parsing error
@ -180,7 +172,7 @@ class PhpDocument
// Report errors from parsing docblocks // Report errors from parsing docblocks
foreach ($docBlockParser->errors as $error) { foreach ($docBlockParser->errors as $error) {
$diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php');
} }
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
@ -198,34 +190,30 @@ class PhpDocument
// Unregister old definitions // Unregister old definitions
if (isset($this->definitions)) { if (isset($this->definitions)) {
foreach ($this->definitions as $fqn => $definition) { foreach ($this->definitions as $fqn => $definition) {
$this->project->removeDefinition($fqn); $this->index->removeDefinition($fqn);
} }
} }
// Register this document on the project for all the symbols defined in it // Register this document on the project for all the symbols defined in it
$this->definitions = $definitionCollector->definitions; $this->definitions = $definitionCollector->definitions;
$this->definitionNodes = $definitionCollector->nodes; $this->definitionNodes = $definitionCollector->nodes;
foreach ($definitionCollector->definitions as $fqn => $definition) { foreach ($definitionCollector->definitions as $fqn => $definition) {
$this->project->setDefinition($fqn, $definition); $this->index->setDefinition($fqn, $definition);
} }
// Unregister old references // Unregister old references
if (isset($this->referenceNodes)) { if (isset($this->referenceNodes)) {
foreach ($this->referenceNodes as $fqn => $node) { foreach ($this->referenceNodes as $fqn => $node) {
$this->project->removeReferenceUri($fqn, $this->uri); $this->index->removeReferenceUri($fqn, $this->uri);
} }
} }
// Register this document on the project for references // Register this document on the project for references
$this->referenceNodes = $referencesCollector->nodes; $this->referenceNodes = $referencesCollector->nodes;
foreach ($referencesCollector->nodes as $fqn => $nodes) { foreach ($referencesCollector->nodes as $fqn => $nodes) {
$this->project->addReferenceUri($fqn, $this->uri); $this->index->addReferenceUri($fqn, $this->uri);
} }
$this->stmts = $stmts; $this->stmts = $stmts;
} }
if (!$this->isVendored()) {
$this->client->textDocument->publishDiagnostics($this->uri, $diagnostics);
}
} }
/** /**
@ -262,6 +250,16 @@ class PhpDocument
return $this->content; return $this->content;
} }
/**
* Returns this document's diagnostics
*
* @return Diagnostic[]
*/
public function getDiagnostics()
{
return $this->diagnostics;
}
/** /**
* Returns the URI of the document * Returns the URI of the document
* *
@ -357,57 +355,4 @@ class PhpDocument
{ {
return isset($this->definitions[$fqn]); return isset($this->definitions[$fqn]);
} }
/**
* Returns the reference nodes for any node
* The references node MAY be in other documents, check the ownerDocument attribute
*
* @param Node $node
* @return Promise <Node[]>
*/
public function getReferenceNodesByNode(Node $node): Promise
{
return coroutine(function () use ($node) {
// Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST
if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param
|| $node instanceof Node\Expr\ClosureUse
) {
if ($node->name instanceof Node\Expr) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
return $refCollector->nodes;
}
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn === null) {
return [];
}
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
$nodes = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$nodes[] = $ref;
}
}
}
return $nodes;
});
}
} }

178
src/PhpDocumentLoader.php Normal file
View File

@ -0,0 +1,178 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\ContentRetriever\ContentRetriever;
use LanguageServer\Index\ProjectIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
/**
* Takes care of loading documents and managing "open" documents
*/
class PhpDocumentLoader
{
/**
* A map from URI => PhpDocument of open documents that should be kept in memory
*
* @var PhpDocument
*/
private $documents = [];
/**
* @var ContentRetriever
*/
private $contentRetriever;
/**
* @var ProjectIndex
*/
private $projectIndex;
/**
* @var Parser
*/
private $parser;
/**
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* @param ContentRetriever $contentRetriever
* @param ProjectIndex $project
* @param DefinitionResolver $definitionResolver
*/
public function __construct(
ContentRetriever $contentRetriever,
ProjectIndex $projectIndex,
DefinitionResolver $definitionResolver
) {
$this->contentRetriever = $contentRetriever;
$this->projectIndex = $projectIndex;
$this->definitionResolver = $definitionResolver;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
}
/**
* Returns the document indicated by uri.
* Returns null if the document if not loaded.
*
* @param string $uri
* @return PhpDocument|null
*/
public function get(string $uri)
{
return $this->documents[$uri] ?? null;
}
/**
* Returns the document indicated by uri.
* If the document is not open, loads it.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function getOrLoad(string $uri): Promise
{
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($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.
* The document is NOT added to the list of open documents, but definitions are registered.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function load(string $uri): Promise
{
return coroutine(function () use ($uri) {
$limit = 150000;
$content = yield $this->contentRetriever->retrieve($uri);
$size = strlen($content);
if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit);
}
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = $this->create($uri, $content);
}
return $document;
});
}
/**
* Builds a PhpDocument instance
*
* @param string $uri
* @param string $content
* @return PhpDocument
*/
public function create(string $uri, string $content): PhpDocument
{
return new PhpDocument(
$uri,
$content,
$this->projectIndex->getIndexForUri($uri),
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
}
/**
* Ensures a document is loaded and added to the list of open documents.
*
* @param string $uri
* @param string $content
* @return void
*/
public function open(string $uri, string $content)
{
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = $this->create($uri, $content);
$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 close(string $uri)
{
unset($this->documents[$uri]);
}
/**
* Returns true if the document is open (and loaded)
*
* @param string $uri
* @return bool
*/
public function isOpen(string $uri): bool
{
return isset($this->documents[$uri]);
}
}

View File

@ -1,349 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities};
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\ContentRetriever\ContentRetriever;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class Project
{
/**
* An associative array [string => PhpDocument]
* that maps URIs to loaded PhpDocuments
*
* @var PhpDocument[]
*/
private $documents = [];
/**
* An associative array that maps fully qualified symbol names to Definitions
*
* @var Definition[]
*/
private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
*
* @var PhpDocument[][]
*/
private $references = [];
/**
* Instance of the PHP parser
*
* @var Parser
*/
private $parser;
/**
* The DocBlockFactory instance to parse docblocks
*
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* The DefinitionResolver instance to resolve reference nodes to Definitions
*
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* Reference to the language server client interface
*
* @var LanguageClient
*/
private $client;
/**
* The content retriever
*
* @var ContentRetriever
*/
private $contentRetriever;
public function __construct(LanguageClient $client, ContentRetriever $contentRetriever)
{
$this->client = $client;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
$this->definitionResolver = new DefinitionResolver($this);
$this->contentRetriever = $contentRetriever;
}
/**
* Returns the document indicated by uri.
* Returns null if the document if not loaded.
*
* @param string $uri
* @return PhpDocument|null
*/
public function getDocument(string $uri)
{
return $this->documents[$uri] ?? null;
}
/**
* 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.
* The document is NOT added to the list of open documents, but definitions are registered.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function loadDocument(string $uri): Promise
{
return coroutine(function () use ($uri) {
$limit = 150000;
$content = yield $this->contentRetriever->retrieve($uri);
$size = strlen($content);
if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit);
}
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
} else {
$document = new PhpDocument(
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
}
return $document;
});
}
/**
* 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 {
$document = new PhpDocument(
$uri,
$content,
$this,
$this->client,
$this->parser,
$this->docBlockFactory,
$this->definitionResolver
);
$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]);
}
/**
* 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
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, $globalFallback = false)
{
if (isset($this->definitions[$fqn])) {
return $this->definitions[$fqn];
} else if ($globalFallback) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
return $this->getDefinition($fqn);
}
}
/**
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
$this->definitions[$fqn] = $definition;
}
/**
* Sets the Definition index
*
* @param Definition[] $definitions Map from FQN to Definition
* @return void
*/
public function setDefinitions(array $definitions)
{
$this->definitions = $definitions;
}
/**
* Unsets the Definition for a specific symbol
* and removes all references pointing to that symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return void
*/
public function removeDefinition(string $fqn)
{
unset($this->definitions[$fqn]);
unset($this->references[$fqn]);
}
/**
* 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;
}
}
/**
* 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
*/
public function removeReferenceUri(string $fqn, string $uri)
{
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);
}
/**
* Returns all documents that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return Promise <PhpDocument[]>
*/
public function getReferenceDocuments(string $fqn): Promise
{
if (!isset($this->references[$fqn])) {
return Promise\resolve([]);
}
return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn]));
}
/**
* 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;
}
/**
* Returns the document where a symbol is defined
*
* @param string $fqn The fully qualified name of the symbol
* @return Promise <PhpDocument|null>
*/
public function getDefinitionDocument(string $fqn): Promise
{
if (!isset($this->definitions[$fqn])) {
return Promise\resolve(null);
}
return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri);
}
/**
* 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]);
}
}

View File

@ -3,9 +3,10 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\Node; use PhpParser\{Node, NodeTraverser};
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
use LanguageServer\NodeVisitor\VariableReferencesCollector;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentItem, TextDocumentItem,
TextDocumentIdentifier, TextDocumentIdentifier,
@ -23,7 +24,9 @@ use LanguageServer\Protocol\{
CompletionItem, CompletionItem,
CompletionItemKind CompletionItemKind
}; };
use LanguageServer\Index\ReadableIndex;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Sabre\Uri;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
/** /**
@ -58,13 +61,29 @@ class TextDocument
*/ */
private $completionProvider; private $completionProvider;
public function __construct(Project $project, LanguageClient $client) /**
{ * @var ReadableIndex
$this->project = $project; */
private $index;
/**
* @param PhpDocumentLoader $documentLoader
* @param DefinitionResolver $definitionResolver
* @param LanguageClient $client
* @param ReadableIndex $index
*/
public function __construct(
PhpDocumentLoader $documentLoader,
DefinitionResolver $definitionResolver,
LanguageClient $client,
ReadableIndex $index
) {
$this->documentLoader = $documentLoader;
$this->client = $client; $this->client = $client;
$this->prettyPrinter = new PrettyPrinter(); $this->prettyPrinter = new PrettyPrinter();
$this->definitionResolver = new DefinitionResolver($project); $this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $project); $this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->index = $index;
} }
/** /**
@ -76,7 +95,7 @@ class TextDocument
*/ */
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
{ {
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) {
$symbols = []; $symbols = [];
foreach ($document->getDefinitions() as $fqn => $definition) { foreach ($document->getDefinitions() as $fqn => $definition) {
$symbols[] = $definition->symbolInformation; $symbols[] = $definition->symbolInformation;
@ -95,7 +114,10 @@ class TextDocument
*/ */
public function didOpen(TextDocumentItem $textDocument) public function didOpen(TextDocumentItem $textDocument)
{ {
$this->project->openDocument($textDocument->uri, $textDocument->text); $document = $this->documentLoader->open($textDocument->uri, $textDocument->text);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
}
} }
/** /**
@ -107,7 +129,9 @@ class TextDocument
*/ */
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
{ {
$this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); $document = $this->documentLoader->get($textDocument->uri);
$document->updateContent($contentChanges[0]->text);
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
} }
/** /**
@ -120,7 +144,7 @@ class TextDocument
*/ */
public function didClose(TextDocumentIdentifier $textDocument) public function didClose(TextDocumentIdentifier $textDocument)
{ {
$this->project->closeDocument($textDocument->uri); $this->documentLoader->close($textDocument->uri);
} }
/** /**
@ -132,7 +156,7 @@ class TextDocument
*/ */
public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options) public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options)
{ {
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) {
return $document->getFormattedText(); return $document->getFormattedText();
}); });
} }
@ -150,15 +174,55 @@ class TextDocument
Position $position Position $position
): Promise { ): Promise {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return [];
} }
$refs = yield $document->getReferenceNodesByNode($node);
$locations = []; $locations = [];
foreach ($refs as $ref) { // Variables always stay in the boundary of the file and need to be searched inside their function scope
$locations[] = Location::fromNode($ref); // by traversing the AST
if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param
|| $node instanceof Node\Expr\ClosureUse
) {
if ($node->name instanceof Node\Expr) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
foreach ($refCollector->nodes as $ref) {
$locations[] = Location::fromNode($ref);
}
} else {
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
if ($fqn === null) {
return [];
}
$refDocuments = yield Promise\all(array_map(
[$this->documentLoader, 'getOrLoad'],
$this->index->getReferenceUris($fqn)
));
foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) {
foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref);
}
}
}
} }
return $locations; return $locations;
}); });
@ -175,13 +239,17 @@ class TextDocument
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return [];
} }
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
if ($def === null || $def->symbolInformation === null) { if (
$def === null
|| $def->symbolInformation === null
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
) {
return []; return [];
} }
return $def->symbolInformation->location; return $def->symbolInformation->location;
@ -198,7 +266,7 @@ class TextDocument
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
// Find the node under the cursor // Find the node under the cursor
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
@ -237,7 +305,7 @@ class TextDocument
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
return $this->completionProvider->provideCompletion($document, $position); return $this->completionProvider->provideCompletion($document, $position);
}); });
} }

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project}; use LanguageServer\{LanguageClient, Project};
use LanguageServer\Index\ProjectIndex;
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
/** /**
@ -19,15 +20,18 @@ class Workspace
private $client; private $client;
/** /**
* The current project database * The symbol index for the workspace
* *
* @var Project * @var ProjectIndex
*/ */
private $project; private $index;
public function __construct(Project $project, LanguageClient $client) /**
* @param ProjectIndex $index Index that is searched on a workspace/symbol request
*/
public function __construct(ProjectIndex $index, LanguageClient $client)
{ {
$this->project = $project; $this->index = $index;
$this->client = $client; $this->client = $client;
} }
@ -40,7 +44,7 @@ class Workspace
public function symbol(string $query): array public function symbol(string $query): array
{ {
$symbols = []; $symbols = [];
foreach ($this->project->getDefinitions() as $fqn => $definition) { foreach ($this->index->getDefinitions() as $fqn => $definition) {
if ($query === '' || stripos($fqn, $query) !== false) { if ($query === '' || stripos($fqn, $query) !== false) {
$symbols[] = $definition->symbolInformation; $symbols[] = $definition->symbolInformation;
} }

View File

@ -6,7 +6,16 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer; use LanguageServer\LanguageServer;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
Message, ClientCapabilities, TextDocumentSyncKind, MessageType, TextDocumentItem, TextDocumentIdentifier}; Message,
ClientCapabilities,
TextDocumentSyncKind,
MessageType,
TextDocumentItem,
TextDocumentIdentifier,
InitializeResult,
ServerCapabilities,
CompletionOptions
};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Webmozart\Glob\Glob; use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
@ -18,41 +27,22 @@ class LanguageServerTest extends TestCase
{ {
public function testInitialize() public function testInitialize()
{ {
$reader = new MockProtocolStream(); $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream);
$writer = new MockProtocolStream(); $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait();
$server = new LanguageServer($reader, $writer);
$promise = new Promise; $serverCapabilities = new ServerCapabilities();
$writer->once('message', [$promise, 'fulfill']); $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
$reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ $serverCapabilities->documentSymbolProvider = true;
'rootPath' => __DIR__, $serverCapabilities->workspaceSymbolProvider = true;
'processId' => getmypid(), $serverCapabilities->documentFormattingProvider = true;
'capabilities' => new ClientCapabilities() $serverCapabilities->definitionProvider = true;
]))); $serverCapabilities->referencesProvider = true;
$msg = $promise->wait(); $serverCapabilities->hoverProvider = true;
$this->assertNotNull($msg, 'message event should be emitted'); $serverCapabilities->completionProvider = new CompletionOptions;
$this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); $serverCapabilities->completionProvider->resolveProvider = false;
$this->assertEquals((object)[ $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
'capabilities' => (object)[
'textDocumentSync' => TextDocumentSyncKind::FULL, $this->assertEquals(new InitializeResult($serverCapabilities), $result);
'documentSymbolProvider' => true,
'hoverProvider' => true,
'completionProvider' => (object)[
'resolveProvider' => false,
'triggerCharacters' => ['$', '>']
],
'signatureHelpProvider' => null,
'definitionProvider' => true,
'referencesProvider' => true,
'documentHighlightProvider' => null,
'workspaceSymbolProvider' => true,
'codeActionProvider' => null,
'codeLensProvider' => null,
'documentFormattingProvider' => true,
'documentRangeFormattingProvider' => null,
'documentOnTypeFormattingProvider' => null,
'renameProvider' => null
]
], $msg->body->result);
} }
public function testIndexingWithDirectFileAccess() public function testIndexingWithDirectFileAccess()

View File

@ -6,9 +6,11 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PhpParser\{NodeTraverser, Node}; use PhpParser\{NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Protocol\ClientCapabilities;
use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex};
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
@ -17,19 +19,25 @@ class DefinitionCollectorTest extends TestCase
{ {
public function testCollectsSymbols() public function testCollectsSymbols()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $path = realpath(__DIR__ . '/../../fixtures/symbols.php');
$project = new Project($client, new FileSystemContentRetriever); $uri = pathToUri($path);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $docBlockFactory = DocBlockFactory::createInstance();
$document = $project->loadDocument($uri)->wait(); $index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver); $traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document)); $traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector); $traverser->addVisitor($definitionCollector);
$stmts = $parser->parse(file_get_contents($uri));
$traverser->traverse($stmts); $traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes; $defNodes = $definitionCollector->nodes;
$this->assertEquals([ $this->assertEquals([
'TestNamespace', 'TestNamespace',
'TestNamespace\\TEST_CONST', 'TestNamespace\\TEST_CONST',
@ -57,19 +65,25 @@ class DefinitionCollectorTest extends TestCase
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $path = realpath(__DIR__ . '/../../fixtures/references.php');
$project = new Project($client, new FileSystemContentRetriever); $uri = pathToUri($path);
$parser = new Parser; $parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $docBlockFactory = DocBlockFactory::createInstance();
$document = $project->loadDocument($uri)->wait(); $index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser; $traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver); $traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document)); $traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); $definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector); $traverser->addVisitor($definitionCollector);
$stmts = $parser->parse(file_get_contents($uri));
$traverser->traverse($stmts); $traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes; $defNodes = $definitionCollector->nodes;
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes)); $this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']); $this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);

View File

@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentItem, TextDocumentItem,
TextDocumentIdentifier, TextDocumentIdentifier,
@ -18,31 +19,35 @@ use LanguageServer\Protocol\{
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
class ProjectTest extends TestCase class PhpDocumentLoaderTest extends TestCase
{ {
/** /**
* @var Project $project * @var PhpDocumentLoader
*/ */
private $project; private $loader;
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->project = new Project($client, new FileSystemContentRetriever); $this->loader = new PhpDocumentLoader(
new FileSystemContentRetriever,
$projectIndex,
new DefinitionResolver($projectIndex)
);
} }
public function testGetOrLoadDocumentLoadsDocument() public function testGetOrLoadLoadsDocument()
{ {
$document = $this->project->getOrLoadDocument(pathToUri(__FILE__))->wait(); $document = $this->loader->getOrLoad(pathToUri(__FILE__))->wait();
$this->assertNotNull($document); $this->assertNotNull($document);
$this->assertInstanceOf(PhpDocument::class, $document); $this->assertInstanceOf(PhpDocument::class, $document);
} }
public function testGetDocumentReturnsOpenedInstance() public function testGetReturnsOpenedInstance()
{ {
$document1 = $this->project->openDocument(pathToUri(__FILE__), file_get_contents(__FILE__)); $document1 = $this->loader->open(pathToUri(__FILE__), file_get_contents(__FILE__));
$document2 = $this->project->getDocument(pathToUri(__FILE__)); $document2 = $this->loader->get(pathToUri(__FILE__));
$this->assertSame($document1, $document2); $this->assertSame($document1, $document2);
} }

View File

@ -4,36 +4,36 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server; namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{LanguageClient, Project}; use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser};
use LanguageServer\NodeVisitor\NodeAtPositionFinder; use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use PhpParser\Node; use PhpParser\Node;
class PhpDocumentTest extends TestCase class PhpDocumentTest extends TestCase
{ {
/** public function createDocument(string $uri, string $content)
* @var Project $project
*/
private $project;
public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $parser = new Parser;
$this->project = new Project($client, new FileSystemContentRetriever); $docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
return new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
} }
public function testParsesVariableVariables() public function testParsesVariableVariables()
{ {
$document = $this->project->openDocument('whatever', "<?php\n$\$a = 'foo';\n\$bar = 'baz';\n"); $document = $this->createDocument('whatever', "<?php\n$\$a = 'foo';\n\$bar = 'baz';\n");
$this->assertEquals([], $document->getDefinitions()); $this->assertEquals([], $document->getDefinitions());
} }
public function testGetNodeAtPosition() public function testGetNodeAtPosition()
{ {
$document = $this->project->openDocument('whatever', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;");
$node = $document->getNodeAtPosition(new Position(1, 13)); $node = $document->getNodeAtPosition(new Position(1, 13));
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node); $this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
$this->assertEquals('SomeClass', (string)$node); $this->assertEquals('SomeClass', (string)$node);
@ -41,19 +41,19 @@ class PhpDocumentTest extends TestCase
public function testIsVendored() public function testIsVendored()
{ {
$document = $this->project->openDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///c:/dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///vendor/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(true, $document->isVendored()); $this->assertEquals(true, $document->isVendored());
$document = $this->project->openDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, $document->isVendored());
$document = $this->project->openDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/x.php', "<?php\n$\$a = new SomeClass;");
$this->assertEquals(false, $document->isVendored()); $this->assertEquals(false, $document->isVendored());
} }
} }

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
@ -24,9 +25,9 @@ abstract class ServerTestCase extends TestCase
protected $workspace; protected $workspace;
/** /**
* @var Project * @var PhpDocumentLoader
*/ */
protected $project; protected $documentLoader;
/** /**
* Map from FQN to Location of definition * Map from FQN to Location of definition
@ -44,10 +45,13 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->project = new Project($client, new FileSystemContentRetriever);
$this->textDocument = new Server\TextDocument($this->project, $client); $definitionResolver = new DefinitionResolver($projectIndex);
$this->workspace = new Server\Workspace($this->project, $client); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$this->workspace = new Server\Workspace($projectIndex, $client);
$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'));
@ -55,11 +59,11 @@ abstract class ServerTestCase extends TestCase
$referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
$useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php')); $useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php'));
$this->project->loadDocument($symbolsUri)->wait(); $this->documentLoader->load($symbolsUri)->wait();
$this->project->loadDocument($referencesUri)->wait(); $this->documentLoader->load($referencesUri)->wait();
$this->project->loadDocument($globalSymbolsUri)->wait(); $this->documentLoader->load($globalSymbolsUri)->wait();
$this->project->loadDocument($globalReferencesUri)->wait(); $this->documentLoader->load($globalReferencesUri)->wait();
$this->project->loadDocument($useUri)->wait(); $this->documentLoader->load($useUri)->wait();
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
$this->definitionLocations = [ $this->definitionLocations = [

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
@ -27,23 +28,26 @@ class CompletionTest extends TestCase
private $textDocument; private $textDocument;
/** /**
* @var Project * @var PhpDocumentLoader
*/ */
private $project; private $loader;
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new FileSystemContentRetriever); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); $definitionResolver = new DefinitionResolver($projectIndex);
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); $contentRetriever = new FileSystemContentRetriever;
$this->textDocument = new Server\TextDocument($this->project, $client); $this->loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver);
$this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait();
$this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait();
$this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex);
} }
public function testPropertyAndMethodWithPrefix() public function testPropertyAndMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 7) new Position(3, 7)
@ -67,7 +71,7 @@ class CompletionTest extends TestCase
public function testPropertyAndMethodWithoutPrefix() public function testPropertyAndMethodWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 6) new Position(3, 6)
@ -91,7 +95,7 @@ class CompletionTest extends TestCase
public function testVariable() public function testVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 5) new Position(8, 5)
@ -123,7 +127,7 @@ class CompletionTest extends TestCase
public function testVariableWithPrefix() public function testVariableWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 6) new Position(8, 6)
@ -145,7 +149,7 @@ class CompletionTest extends TestCase
public function testNewInNamespace() public function testNewInNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 10) new Position(6, 10)
@ -177,7 +181,7 @@ class CompletionTest extends TestCase
public function testUsedClass() public function testUsedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 5) new Position(6, 5)
@ -195,7 +199,7 @@ class CompletionTest extends TestCase
public function testStaticPropertyWithPrefix() public function testStaticPropertyWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 14) new Position(2, 14)
@ -216,7 +220,7 @@ class CompletionTest extends TestCase
public function testStaticWithoutPrefix() public function testStaticWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 11) new Position(2, 11)
@ -249,7 +253,7 @@ class CompletionTest extends TestCase
public function testStaticMethodWithPrefix() public function testStaticMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 13) new Position(2, 13)
@ -282,7 +286,7 @@ class CompletionTest extends TestCase
public function testClassConstWithPrefix() public function testClassConstWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 13) new Position(2, 13)
@ -315,7 +319,7 @@ class CompletionTest extends TestCase
public function testFullyQualifiedClass() public function testFullyQualifiedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 6) new Position(6, 6)
@ -336,7 +340,7 @@ class CompletionTest extends TestCase
public function testKeywords() public function testKeywords()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 1) new Position(2, 1)
@ -350,7 +354,7 @@ class CompletionTest extends TestCase
public function testHtmlWithoutPrefix() public function testHtmlWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 0) new Position(0, 0)
@ -372,7 +376,7 @@ class CompletionTest extends TestCase
public function testHtmlWithPrefix() public function testHtmlWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 1) new Position(0, 1)
@ -394,7 +398,7 @@ class CompletionTest extends TestCase
public function testNamespace() public function testNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php');
$this->project->openDocument($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion( $items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(4, 6) new Position(4, 6)

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities};
use Sabre\Event\Promise; use Sabre\Event\Promise;
@ -14,11 +15,14 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new FileSystemContentRetriever); $definitionResolver = new DefinitionResolver($projectIndex);
$this->textDocument = new Server\TextDocument($project, $client); $contentRetriever = new FileSystemContentRetriever;
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver);
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$loader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$loader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
} }
public function testClassDoesNotFallback() public function testClassDoesNotFallback()

View File

@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextDocumentItem, TextDocumentItem,
@ -21,10 +22,12 @@ class DidChangeTest extends TestCase
{ {
public function test() public function test()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new FileSystemContentRetriever); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n"); $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$phpDocument = $loader->open('whatever', "<?php\necho 'Hello, World'\n");
$identifier = new VersionedTextDocumentIdentifier('whatever'); $identifier = new VersionedTextDocumentIdentifier('whatever');
$changeEvent = new TextDocumentContentChangeEvent(); $changeEvent = new TextDocumentContentChangeEvent();

View File

@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
use Exception; use Exception;
@ -14,10 +15,12 @@ class DidCloseTest extends TestCase
{ {
public function test() public function test()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new FileSystemContentRetriever); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$phpDocument = $project->openDocument('whatever', 'hello world'); $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$phpDocument = $loader->open('whatever', "<?php\necho 'Hello, World'\n");
$textDocumentItem = new TextDocumentItem(); $textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever'; $textDocumentItem->uri = 'whatever';
@ -28,6 +31,6 @@ class DidCloseTest extends TestCase
$textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri)); $textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri));
$this->assertFalse($project->isDocumentOpen($textDocumentItem->uri)); $this->assertFalse($loader->isOpen($textDocumentItem->uri));
} }
} }

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
@ -20,23 +21,14 @@ use function LanguageServer\{pathToUri, uriToPath};
class FormattingTest extends TestCase class FormattingTest extends TestCase
{ {
/**
* @var Server\TextDocument
*/
private $textDocument;
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new FileSystemContentRetriever);
$this->textDocument = new Server\TextDocument($project, $client);
}
public function testFormatting() public function testFormatting()
{ {
$projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client, new FileSystemContentRetriever); $definitionResolver = new DefinitionResolver($projectIndex);
$textDocument = new Server\TextDocument($project, $client); $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
$path = realpath(__DIR__ . '/../../../fixtures/format.php'); $path = realpath(__DIR__ . '/../../../fixtures/format.php');
$uri = pathToUri($path); $uri = pathToUri($path);

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; use LanguageServer\{Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities};
use Sabre\Event\Promise; use Sabre\Event\Promise;
@ -36,8 +37,10 @@ class ParseErrorsTest extends TestCase
return Promise\resolve(null); return Promise\resolve(null);
} }
}; };
$project = new Project($client, new FileSystemContentRetriever); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$this->textDocument = new Server\TextDocument($project, $client); $definitionResolver = new DefinitionResolver($projectIndex);
$loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex);
} }
private function openFile($file) private function openFile($file)

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\References;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities};
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
@ -14,11 +15,13 @@ class GlobalFallbackTest extends ServerTestCase
{ {
public function setUp() public function setUp()
{ {
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $projectIndex = new ProjectIndex(new Index, new DependenciesIndex);
$project = new Project($client, new FileSystemContentRetriever); $definitionResolver = new DefinitionResolver($projectIndex);
$this->textDocument = new Server\TextDocument($project, $client); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$this->documentLoader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$this->documentLoader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
} }
public function testClassDoesNotFallback() public function testClassDoesNotFallback()