1
0
Fork 0

Merge branch 'master' into fixes-only

pull/625/head
Felix Becker 2018-11-11 20:25:04 +01:00 committed by GitHub
commit cdeacfd8e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 1170 additions and 2072 deletions

View File

@ -4,7 +4,6 @@
[![Linux Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server) [![Linux Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server)
[![Windows Build status](https://ci.appveyor.com/api/projects/status/2sp5ll052wdjqmdm/branch/master?svg=true)](https://ci.appveyor.com/project/felixfbecker/php-language-server/branch/master) [![Windows Build status](https://ci.appveyor.com/api/projects/status/2sp5ll052wdjqmdm/branch/master?svg=true)](https://ci.appveyor.com/project/felixfbecker/php-language-server/branch/master)
[![Coverage](https://codecov.io/gh/felixfbecker/php-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/php-language-server) [![Coverage](https://codecov.io/gh/felixfbecker/php-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/php-language-server)
[![Dependency Status](https://gemnasium.com/badges/github.com/felixfbecker/php-language-server.svg)](https://gemnasium.com/github.com/felixfbecker/php-language-server)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/) [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/)
[![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt) [![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt)

89
benchmarks/completion.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace LanguageServer\Tests;
require __DIR__ . '/../vendor/autoload.php';
use Composer\XdebugHandler\XdebugHandler;
use Exception;
use LanguageServer\CompletionProvider;
use LanguageServer\DefinitionResolver;
use LanguageServer\Index\Index;
use LanguageServer\PhpDocument;
use LanguageServer\StderrLogger;
use LanguageServerProtocol\Position;
use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
$logger = new StderrLogger();
$xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
$totalSize = 0;
$framework = "symfony";
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
$testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) {
if (strpos((string)$file, ".php") !== false) {
$totalSize += $file->getSize();
$testProviderArray[] = $file->getRealPath();
}
}
if (count($testProviderArray) === 0) {
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
}
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$completionProvider = new CompletionProvider($definitionResolver, $index);
$docBlockFactory = DocBlockFactory::createInstance();
$completionFile = realpath(__DIR__ . '/../validation/frameworks/symfony/src/Symfony/Component/HttpFoundation/Request.php');
$parser = new PhpParser\Parser();
$completionDocument = null;
echo "Indexing $framework" . PHP_EOL;
foreach ($testProviderArray as $idx => $testCaseFile) {
if (filesize($testCaseFile) > 100000) {
continue;
}
if ($idx % 100 === 0) {
echo $idx . '/' . count($testProviderArray) . PHP_EOL;
}
$fileContents = file_get_contents($testCaseFile);
try {
$d = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
if ($testCaseFile === $completionFile) {
$completionDocument = $d;
}
} catch (\Throwable $e) {
echo $e->getMessage() . PHP_EOL;
continue;
}
}
echo "Getting completion". PHP_EOL;
// Completion in $this->|request = new ParameterBag($request);
$start = microtime(true);
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 15));
$end = microtime(true);
echo 'Time ($this->|): ' . ($end - $start) . 's' . PHP_EOL;
echo count($list->items) . ' completion items' . PHP_EOL;
// Completion in $this->request = new| ParameterBag($request);
// (this only finds ParameterBag though.)
$start = microtime(true);
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 28));
$end = microtime(true);
echo 'Time (new|): ' . ($end - $start) . 's' . PHP_EOL;
echo count($list->items) . ' completion items' . PHP_EOL;

View File

@ -1,23 +1,31 @@
<?php <?php
namespace LanguageServer\Tests; namespace LanguageServer\Tests;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
use Composer\XdebugHandler\XdebugHandler;
use Exception; use Exception;
use LanguageServer\DefinitionResolver;
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use LanguageServer\PhpDocument; use LanguageServer\PhpDocument;
use LanguageServer\DefinitionResolver; use LanguageServer\StderrLogger;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
$logger = new StderrLogger();
$xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
$totalSize = 0; $totalSize = 0;
$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"]; $frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"];
foreach($frameworks as $framework) { foreach($frameworks as $framework) {
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework"); $iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
$testProviderArray = array(); $testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) { foreach (new RecursiveIteratorIterator($iterator) as $file) {
@ -37,8 +45,8 @@ foreach($frameworks as $framework) {
if (filesize($testCaseFile) > 10000) { if (filesize($testCaseFile) > 10000) {
continue; continue;
} }
if ($idx % 1000 === 0) { if ($idx % 500 === 0) {
echo "$idx\n"; echo $idx . '/' . count($testProviderArray) . PHP_EOL;
} }
$fileContents = file_get_contents($testCaseFile); $fileContents = file_get_contents($testCaseFile);

View File

@ -24,6 +24,7 @@
"php": "^7.0", "php": "^7.0",
"composer/xdebug-handler": "^1.0", "composer/xdebug-handler": "^1.0",
"felixfbecker/advanced-json-rpc": "^3.0.0", "felixfbecker/advanced-json-rpc": "^3.0.0",
"felixfbecker/language-server-protocol": "^1.0.1",
"jetbrains/phpstorm-stubs": "dev-master", "jetbrains/phpstorm-stubs": "dev-master",
"microsoft/tolerant-php-parser": "0.0.*", "microsoft/tolerant-php-parser": "0.0.*",
"netresearch/jsonmapper": "^1.0", "netresearch/jsonmapper": "^1.0",

View File

@ -0,0 +1,10 @@
<?php
namespace Whatever;
use TestNamespace\InnerNamespace as AliasNamespace;
class IDontShowUpInCompletion {}
AliasNamespace\I;
AliasNamespace\;

View File

@ -0,0 +1,20 @@
<?php
namespace RecursiveTest;
class A extends A {}
class B extends C {}
class C extends B {}
class D extends E {}
class E extends F {}
class F extends D {}
$a = new A;
$a->undef_prop = 1;
$b = new B;
$b->undef_prop = 1;
$d = new D;
$d->undef_prop = 1;

View File

@ -103,3 +103,8 @@ class Example {
public function __construct() {} public function __construct() {}
public function __destruct() {} public function __destruct() {}
} }
namespace TestNamespace\InnerNamespace;
class InnerClass {
}

4
package-lock.json generated
View File

@ -1,8 +1,6 @@
{ {
"name": "php-language-server",
"version": "0.0.0-development",
"lockfileVersion": 1,
"requires": true, "requires": true,
"lockfileVersion": 1,
"dependencies": { "dependencies": {
"@gimenete/type-writer": { "@gimenete/type-writer": {
"version": "0.1.3", "version": "0.1.3",

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier}; use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier};
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\TextDocumentIdentifier; use LanguageServerProtocol\TextDocumentIdentifier;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;

View File

@ -41,12 +41,12 @@ class ClientHandler
{ {
$id = $this->idGenerator->generate(); $id = $this->idGenerator->generate();
return $this->protocolWriter->write( return $this->protocolWriter->write(
new Protocol\Message( new Message(
new AdvancedJsonRpc\Request($id, $method, (object)$params) new AdvancedJsonRpc\Request($id, $method, (object)$params)
) )
)->then(function () use ($id) { )->then(function () use ($id) {
$promise = new Promise; $promise = new Promise;
$listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) { $listener = function (Message $msg) use ($id, $promise, &$listener) {
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) { if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
// Received a response // Received a response
$this->protocolReader->removeListener('message', $listener); $this->protocolReader->removeListener('message', $listener);
@ -72,7 +72,7 @@ class ClientHandler
public function notify(string $method, $params): Promise public function notify(string $method, $params): Promise
{ {
return $this->protocolWriter->write( return $this->protocolWriter->write(
new Protocol\Message( new Message(
new AdvancedJsonRpc\Notification($method, (object)$params) new AdvancedJsonRpc\Notification($method, (object)$params)
) )
); );

View File

@ -4,7 +4,8 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServer\Factory\CompletionItemFactory;
use LanguageServerProtocol\{
TextEdit, TextEdit,
Range, Range,
Position, Position,
@ -16,7 +17,15 @@ use LanguageServer\Protocol\{
}; };
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\ResolvedName;
use Generator; use Generator;
use function LanguageServer\FqnUtilities\{
nameConcat,
nameGetFirstPart,
nameGetParent,
nameStartsWith,
nameWithoutFirstPart
};
class CompletionProvider class CompletionProvider
{ {
@ -143,8 +152,11 @@ class CompletionProvider
* @param CompletionContext $context The completion context * @param CompletionContext $context The completion context
* @return CompletionList * @return CompletionList
*/ */
public function provideCompletion(PhpDocument $doc, Position $pos, CompletionContext $context = null): CompletionList public function provideCompletion(
{ PhpDocument $doc,
Position $pos,
CompletionContext $context = null
): CompletionList {
// This can be made much more performant if the tree follows specific invariants. // This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos); $node = $doc->getNodeAtPosition($pos);
@ -236,17 +248,15 @@ class CompletionProvider
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression) $this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
); );
// Add the object access operator to only get members of all parents // The FQNs of the symbol and its parents (eg the implemented interfaces)
$prefixes = []; foreach ($this->expandParentFqns($fqns) as $parentFqn) {
foreach ($this->expandParentFqns($fqns) as $prefix) { // Add the object access operator to only get members of all parents
$prefixes[] = $prefix . '->'; $prefix = $parentFqn . '->';
} $prefixLen = strlen($prefix);
// Collect fqn definitions
// Collect all definitions that match any of the prefixes foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
foreach ($this->index->getDefinitions() as $fqn => $def) { if (substr($fqn, 0, $prefixLen) === $prefix && $def->isMember) {
foreach ($prefixes as $prefix) { $list->items[] = CompletionItemFactory::fromDefinition($def);
if (substr($fqn, 0, strlen($prefix)) === $prefix && $def->isMember) {
$list->items[] = CompletionItem::fromDefinition($def);
} }
} }
} }
@ -269,17 +279,15 @@ class CompletionProvider
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier) $classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
); );
// Append :: operator to only get static members of all parents // The FQNs of the symbol and its parents (eg the implemented interfaces)
$prefixes = []; foreach ($this->expandParentFqns($fqns) as $parentFqn) {
foreach ($this->expandParentFqns($fqns) as $prefix) { // Append :: operator to only get static members of all parents
$prefixes[] = $prefix . '::'; $prefix = strtolower($parentFqn . '::');
} $prefixLen = strlen($prefix);
// Collect fqn definitions
// Collect all definitions that match any of the prefixes foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
foreach ($this->index->getDefinitions() as $fqn => $def) { if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) {
foreach ($prefixes as $prefix) { $list->items[] = CompletionItemFactory::fromDefinition($def);
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) && $def->isMember) {
$list->items[] = CompletionItem::fromDefinition($def);
} }
} }
} }
@ -296,114 +304,278 @@ class CompletionProvider
// my_func| // my_func|
// MY_CONS| // MY_CONS|
// MyCla| // MyCla|
// \MyCla|
// The name Node under the cursor // The name Node under the cursor
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node; $nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
/** The typed name */ if ($nameNode instanceof Node\QualifiedName) {
$prefix = $nameNode instanceof Node\QualifiedName /** @var string The typed name. */
? (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents()) $prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents());
: $nameNode->getText($node->getFileContents()); } else {
$prefixLen = strlen($prefix); $prefix = $nameNode->getText($node->getFileContents());
}
/** Whether the prefix is qualified (contains at least one backslash) */
$isQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isQualifiedName();
/** Whether the prefix is fully qualified (begins with a backslash) */
$isFullyQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isFullyQualifiedName();
/** The closest NamespaceDefinition Node */
$namespaceNode = $node->getNamespaceDefinition(); $namespaceNode = $node->getNamespaceDefinition();
/** @var string The current namespace without a leading backslash. */
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
/** @var string The name of the namespace */ /** @var bool Whether the prefix is qualified (contains at least one backslash) */
$namespacedPrefix = null; $isFullyQualified = false;
if ($namespaceNode) {
$namespacedPrefix = (string)PhpParser\ResolvedName::buildName($namespaceNode->name->nameParts, $node->getFileContents()) . '\\' . $prefix; /** @var bool Whether the prefix is qualified (contains at least one backslash) */
$namespacedPrefixLen = strlen($namespacedPrefix); $isQualified = false;
if ($nameNode instanceof Node\QualifiedName) {
$isFullyQualified = $nameNode->isFullyQualifiedName();
$isQualified = $nameNode->isQualifiedName();
} }
// Get the namespace use statements /** @var bool Whether we are in a new expression */
// TODO: use function statements, use const statements $isCreation = isset($creation);
/** @var string[] $aliases A map from local alias to fully qualified name */ /** @var array Import (use) tables */
list($aliases,,) = $node->getImportTablesForCurrentScope(); $importTables = $node->getImportTablesForCurrentScope();
foreach ($aliases as $alias => $name) { if ($isFullyQualified) {
$aliases[$alias] = (string)$name; // \Prefix\Goes\Here| - Only return completions from the root namespace.
/** @var $items \Generator|CompletionItem[] Generator yielding CompletionItems indexed by their FQN */
$items = $this->getCompletionsForFqnPrefix($prefix, $isCreation, false);
} else if ($isQualified) {
// Prefix\Goes\Here|
$items = $this->getPartiallyQualifiedCompletions(
$prefix,
$currentNamespace,
$importTables,
$isCreation
);
} else {
// PrefixGoesHere|
$items = $this->getUnqualifiedCompletions($prefix, $currentNamespace, $importTables, $isCreation);
} }
// If there is a prefix that does not start with a slash, suggest `use`d symbols $list->items = array_values(iterator_to_array($items));
if ($prefix && !$isFullyQualified) { foreach ($list->items as $item) {
foreach ($aliases as $alias => $fqn) { // Remove ()
// Suggest symbols that have been `use`d and match the prefix if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
if (substr($alias, 0, $prefixLen) === $prefix && ($def = $this->index->getDefinition($fqn))) { $item->insertText = substr($item->insertText, 0, -2);
$list->items[] = CompletionItem::fromDefinition($def);
}
} }
} }
// Suggest global symbols that either }
// - start with the current namespace + prefix, if the Name node is not fully qualified return $list;
// - start with just the prefix, if the Name node is fully qualified }
foreach ($this->index->getDefinitions() as $fqn => $def) {
$fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix; private function getPartiallyQualifiedCompletions(
string $prefix,
if ( string $currentNamespace,
// Exclude methods, properties etc. array $importTables,
!$def->isMember bool $requireCanBeInstantiated
&& ( ): \Generator {
!$prefix // If the first part of the partially qualified name matches a namespace alias,
|| ( // only definitions below that alias can be completed.
// Either not qualified, but a matching prefix with global fallback list($namespaceAliases,,) = $importTables;
($def->roamed && !$isQualified && $fqnStartsWithPrefix) $prefixFirstPart = nameGetFirstPart($prefix);
// Or not in a namespace or a fully qualified name or AND matching the prefix $foundAlias = $foundAliasFqn = null;
|| ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix) foreach ($namespaceAliases as $alias => $aliasFqn) {
// Or in a namespace, not fully qualified and matching the prefix + current namespace if (strcasecmp($prefixFirstPart, $alias) === 0) {
|| ( $foundAlias = $alias;
$namespaceNode $foundAliasFqn = (string)$aliasFqn;
&& !$isFullyQualified break;
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
)
// Only suggest classes for `new`
&& (!isset($creation) || $def->canBeInstantiated)
) {
$item = CompletionItem::fromDefinition($def);
// Find the shortest name to reference the symbol
if ($namespaceNode && ($alias = array_search($fqn, $aliases, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace
$item->insertText = $alias;
} else if ($namespaceNode && !($prefix && $isFullyQualified)) {
// Insert the global FQN with leading backslash
$item->insertText = '\\' . $fqn;
} else {
// Insert the FQN without leading backlash
$item->insertText = $fqn;
}
// Don't insert the parenthesis for functions
// TODO return a snippet and put the cursor inside
if (substr($item->insertText, -2) === '()') {
$item->insertText = substr($item->insertText, 0, -2);
}
$list->items[] = $item;
}
}
// If not a class instantiation, also suggest keywords
if (!isset($creation)) {
foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
$list->items[] = $item;
}
}
} }
} }
return $list; if ($foundAlias !== null) {
yield from $this->getCompletionsFromAliasedNamespace(
$prefix,
$foundAlias,
$foundAliasFqn,
$requireCanBeInstantiated
);
} else {
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
}
}
/**
* Yields completions for non-qualified global names.
*
* Yields
* - Aliased classes
* - Completions from current namespace
* - Roamed completions from the global namespace (when not creating and not already in root NS)
* - PHP keywords (when not creating)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems
*/
private function getUnqualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// Aliases
list($namespaceAliases,,) = $importTables;
// use Foo\Bar
yield from $this->getCompletionsForAliases(
$prefix,
$namespaceAliases,
$requireCanBeInstantiated
);
// Completions from the current namespace
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
if ($currentNamespace !== '' && $prefix === '') {
// Get additional suggestions from the global namespace.
// When completing e.g. for new |, suggest \DateTime
yield from $this->getCompletionsForFqnPrefix('', $requireCanBeInstantiated, true);
}
if (!$requireCanBeInstantiated) {
if ($currentNamespace !== '' && $prefix !== '') {
// Roamed definitions (i.e. global constants and functions). The prefix is checked against '', since
// in that case global completions have already been provided (including non-roamed definitions.)
yield from $this->getRoamedCompletions($prefix);
}
// Lastly and least importantly, suggest keywords.
yield from $this->getCompletionsForKeywords($prefix);
}
}
/**
* Gets completions for prefixes of fully qualified names in their parent namespace.
*
* @param string $prefix Prefix to complete for. Fully qualified.
* @param bool $requireCanBeInstantiated If set, only return classes.
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForFqnPrefix(
string $prefix,
bool $requireCanBeInstantiated,
bool $insertFullyQualified
): \Generator {
$namespace = nameGetParent($prefix);
foreach ($this->index->getChildDefinitionsForFqn($namespace) as $fqn => $def) {
if ($requireCanBeInstantiated && !$def->canBeInstantiated) {
continue;
}
if (!nameStartsWith($fqn, $prefix)) {
continue;
}
$completion = CompletionItemFactory::fromDefinition($def);
if ($insertFullyQualified) {
$completion->insertText = '\\' . $fqn;
}
yield $fqn => $completion;
}
}
/**
* Gets completions for non-qualified names matching the start of an used class, function, or constant.
*
* @param string $prefix Non-qualified name being completed for
* @param QualifiedName[] $aliases Array of alias FQNs indexed by the alias.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForAliases(
string $prefix,
array $aliases,
bool $requireCanBeInstantiated
): \Generator {
foreach ($aliases as $alias => $aliasFqn) {
if (!nameStartsWith($alias, $prefix)) {
continue;
}
$definition = $this->index->getDefinition((string)$aliasFqn);
if ($definition) {
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($definition);
$completionItem->insertText = $alias;
yield (string)$aliasFqn => $completionItem;
}
}
}
/**
* Gets completions for partially qualified names, where the first part is matched by an alias.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsFromAliasedNamespace(
string $prefix,
string $alias,
string $aliasFqn,
bool $requireCanBeInstantiated
): \Generator {
$prefixFirstPart = nameGetFirstPart($prefix);
// Matched alias.
$resolvedPrefix = nameConcat($aliasFqn, nameWithoutFirstPart($prefix));
$completionItems = $this->getCompletionsForFqnPrefix(
$resolvedPrefix,
$requireCanBeInstantiated,
false
);
// Convert FQNs in the CompletionItems so they are expressed in terms of the alias.
foreach ($completionItems as $fqn => $completionItem) {
/** @var string $fqn with the leading parts determined by the alias removed. Has the leading backslash. */
$nameWithoutAliasedPart = substr($fqn, strlen($aliasFqn));
$completionItem->insertText = $alias . $nameWithoutAliasedPart;
yield $fqn => $completionItem;
}
}
/**
* Gets completions for globally defined functions and constants (i.e. symbols which may be used anywhere)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getRoamedCompletions(string $prefix): \Generator
{
foreach ($this->index->getChildDefinitionsForFqn('') as $fqn => $def) {
if (!$def->roamed || !nameStartsWith($fqn, $prefix)) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($def);
// Second-guessing the user here - do not trust roaming to work. If the same symbol is
// inserted in the current namespace, the code will stop working.
$completionItem->insertText = '\\' . $fqn;
yield $fqn => $completionItem;
}
}
/**
* Completes PHP keywords.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForKeywords(string $prefix): \Generator
{
foreach (self::KEYWORDS as $keyword) {
if (nameStartsWith($keyword, $prefix)) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
yield $keyword => $item;
}
}
} }
/** /**
@ -472,8 +644,9 @@ class CompletionProvider
} }
} }
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null && if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression
$level->anonymousFunctionUseClause->useVariableNameList !== null) { && $level->anonymousFunctionUseClause !== null
&& $level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) { foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName(); $useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) { if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\ContentRetriever; namespace LanguageServer\ContentRetriever;
use LanguageServer\LanguageClient; use LanguageServer\LanguageClient;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem}; use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem};
use Sabre\Event\Promise; use Sabre\Event\Promise;
/** /**

View File

@ -5,7 +5,7 @@ namespace LanguageServer;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use phpDocumentor\Reflection\{Types, Type, TypeResolver}; use phpDocumentor\Reflection\{Types, Type, TypeResolver};
use LanguageServer\Protocol\SymbolInformation; use LanguageServerProtocol\SymbolInformation;
use Generator; use Generator;
/** /**

View File

@ -4,7 +4,8 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Factory\SymbolInformationFactory;
use LanguageServerProtocol\SymbolInformation;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\FunctionLike; use Microsoft\PhpParser\FunctionLike;
@ -36,7 +37,7 @@ class DefinitionResolver
private $docBlockFactory; private $docBlockFactory;
/** /**
* Creates SignatureInformation * Creates SignatureInformation instances
* *
* @var SignatureInformationFactory * @var SignatureInformationFactory
*/ */
@ -233,7 +234,7 @@ class DefinitionResolver
} }
} }
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn); $def->symbolInformation = SymbolInformationFactory::fromNode($node, $fqn);
if ($def->symbolInformation !== null) { if ($def->symbolInformation !== null) {
$def->type = $this->getTypeFromNode($node); $def->type = $this->getTypeFromNode($node);
@ -437,6 +438,7 @@ class DefinitionResolver
// Find the right class that implements the member // Find the right class that implements the member
$implementorFqns = [$classFqn]; $implementorFqns = [$classFqn];
$visitedFqns = [];
while ($implementorFqn = array_shift($implementorFqns)) { while ($implementorFqn = array_shift($implementorFqns)) {
// If the member FQN exists, return it // If the member FQN exists, return it
@ -449,10 +451,15 @@ class DefinitionResolver
if ($implementorDef === null) { if ($implementorDef === null) {
break; break;
} }
// Note the FQN as visited
$visitedFqns[] = $implementorFqn;
// Repeat for parent class // Repeat for parent class
if ($implementorDef->extends) { if ($implementorDef->extends) {
foreach ($implementorDef->extends as $extends) { foreach ($implementorDef->extends as $extends) {
$implementorFqns[] = $extends; // Don't add the parent FQN if it's already been visited
if (!\in_array($extends, $visitedFqns)) {
$implementorFqns[] = $extends;
}
} }
} }
} }
@ -1232,7 +1239,13 @@ class DefinitionResolver
if ( if (
$node instanceof PhpParser\ClassLike $node instanceof PhpParser\ClassLike
) { ) {
return (string) $node->getNamespacedName(); $className = (string)$node->getNamespacedName();
// An (invalid) class declaration without a name will have an empty string as name,
// but should not define an FQN
if ($className === '') {
return null;
}
return $className;
} }
// INPUT OUTPUT: // INPUT OUTPUT:

View File

@ -0,0 +1,36 @@
<?php
namespace LanguageServer\Factory;
use LanguageServer\Definition;
use LanguageServerProtocol\CompletionItem;
use LanguageServerProtocol\CompletionItemKind;
use LanguageServerProtocol\SymbolKind;
class CompletionItemFactory
{
/**
* Creates a CompletionItem for a Definition
*
* @param Definition $def
* @return CompletionItem|null
*/
public static function fromDefinition(Definition $def)
{
$item = new CompletionItem;
$item->label = $def->symbolInformation->name;
$item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind);
if ($def->type) {
$item->detail = (string)$def->type;
} else if ($def->symbolInformation->containerName) {
$item->detail = $def->symbolInformation->containerName;
}
if ($def->documentation) {
$item->documentation = $def->documentation;
}
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
$item->insertText = '$' . $def->symbolInformation->name;
}
return $item;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace LanguageServer\Factory;
use LanguageServerProtocol\Location;
use LanguageServerProtocol\Position;
use LanguageServerProtocol\Range;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\PositionUtilities;
class LocationFactory
{
/**
* Returns the location of the node
*
* @param Node $node
* @return self
*/
public static function fromNode(Node $node): Location
{
$range = PositionUtilities::getRangeFromPosition(
$node->getStart(),
$node->getWidth(),
$node->getFileContents()
);
return new Location($node->getUri(), new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
));
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace LanguageServer\Factory;
use LanguageServerProtocol\Position;
use LanguageServerProtocol\Range;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\PositionUtilities;
class RangeFactory
{
/**
* Returns the range the node spans
*
* @param Node $node
* @return self
*/
public static function fromNode(Node $node)
{
$range = PositionUtilities::getRangeFromPosition(
$node->getStart(),
$node->getWidth(),
$node->getFileContents()
);
return new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
);
}
}

View File

@ -1,44 +1,16 @@
<?php <?php
namespace LanguageServer\Protocol; namespace LanguageServer\Factory;
use Microsoft\PhpParser; use LanguageServerProtocol\Location;
use LanguageServerProtocol\SymbolInformation;
use LanguageServerProtocol\SymbolKind;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\ResolvedName;
use LanguageServer\Factory\LocationFactory;
/** class SymbolInformationFactory
* Represents information about programming constructs like variables, classes,
* interfaces etc.
*/
class SymbolInformation
{ {
/**
* The name of this symbol.
*
* @var string
*/
public $name;
/**
* The kind of this symbol.
*
* @var int
*/
public $kind;
/**
* The location of this symbol.
*
* @var Location
*/
public $location;
/**
* The name of the symbol containing this symbol.
*
* @var string|null
*/
public $containerName;
/** /**
* Converts a Node to a SymbolInformation * Converts a Node to a SymbolInformation
* *
@ -48,7 +20,7 @@ class SymbolInformation
*/ */
public static function fromNode($node, string $fqn = null) public static function fromNode($node, string $fqn = null)
{ {
$symbol = new self; $symbol = new SymbolInformation();
if ($node instanceof Node\Statement\ClassDeclaration) { if ($node instanceof Node\Statement\ClassDeclaration) {
$symbol->kind = SymbolKind::CLASS_; $symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Statement\TraitDeclaration) { } else if ($node instanceof Node\Statement\TraitDeclaration) {
@ -98,7 +70,7 @@ class SymbolInformation
$symbol->name = $node->getName(); $symbol->name = $node->getName();
} else if (isset($node->name)) { } else if (isset($node->name)) {
if ($node->name instanceof Node\QualifiedName) { if ($node->name instanceof Node\QualifiedName) {
$symbol->name = (string)PhpParser\ResolvedName::buildName($node->name->nameParts, $node->getFileContents()); $symbol->name = (string)ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
} else { } else {
$symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$"); $symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$");
} }
@ -108,7 +80,7 @@ class SymbolInformation
return null; return null;
} }
$symbol->location = Location::fromNode($node); $symbol->location = LocationFactory::fromNode($node);
if ($fqn !== null) { if ($fqn !== null) {
$parts = preg_split('/(::|->|\\\\)/', $fqn); $parts = preg_split('/(::|->|\\\\)/', $fqn);
array_pop($parts); array_pop($parts);
@ -116,18 +88,4 @@ class SymbolInformation
} }
return $symbol; return $symbol;
} }
/**
* @param string $name
* @param int $kind
* @param Location $location
* @param string $containerName
*/
public function __construct($name = null, $kind = null, $location = null, $containerName = null)
{
$this->name = $name;
$this->kind = $kind;
$this->location = $location;
$this->containerName = $containerName;
}
} }

View File

@ -28,3 +28,91 @@ function getFqnsFromType($type): array
} }
return $fqns; return $fqns;
} }
/**
* Returns parent of an FQN.
*
* getFqnParent('') === ''
* getFqnParent('\\') === ''
* getFqnParent('\A') === ''
* getFqnParent('A') === ''
* getFqnParent('\A\') === '\A' // Empty trailing name is considered a name.
*
* @return string
*/
function nameGetParent(string $name): string
{
if ($name === '') { // Special-case handling for the root namespace.
return '';
}
$parts = explode('\\', $name);
array_pop($parts);
return implode('\\', $parts);
}
/**
* Concatenates two names.
*
* nameConcat('\Foo\Bar', 'Baz') === '\Foo\Bar\Baz'
* nameConcat('\Foo\Bar\\', '\Baz') === '\Foo\Bar\Baz'
* nameConcat('\\', 'Baz') === '\Baz'
* nameConcat('', 'Baz') === 'Baz'
*
* @return string
*/
function nameConcat(string $a, string $b): string
{
if ($a === '') {
return $b;
}
$a = rtrim($a, '\\');
$b = ltrim($b, '\\');
return "$a\\$b";
}
/**
* Returns the first component of $name.
*
* nameGetFirstPart('Foo\Bar') === 'Foo'
* nameGetFirstPart('\Foo\Bar') === 'Foo'
* nameGetFirstPart('') === ''
* nameGetFirstPart('\') === ''
*/
function nameGetFirstPart(string $name): string
{
$parts = explode('\\', $name, 3);
if ($parts[0] === '' && count($parts) > 1) {
return $parts[1];
} else {
return $parts[0];
}
}
/**
* Removes the first component of $name.
*
* nameWithoutFirstPart('Foo\Bar') === 'Bar'
* nameWithoutFirstPart('\Foo\Bar') === 'Bar'
* nameWithoutFirstPart('') === ''
* nameWithoutFirstPart('\') === ''
*/
function nameWithoutFirstPart(string $name): string
{
$parts = explode('\\', $name, 3);
if ($parts[0] === '') {
array_shift($parts);
}
array_shift($parts);
return implode('\\', $parts);
}
/**
* @param string $name Name to match against
* @param string $prefix Prefix $name has to starts with
* @return bool
*/
function nameStartsWith(string $name, string $prefix): bool
{
return strlen($name) >= strlen($prefix)
&& strncmp($name, $prefix, strlen($prefix)) === 0;
}

View File

@ -99,20 +99,29 @@ abstract class AbstractAggregateIndex implements ReadableIndex
} }
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definition[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array public function getDefinitions(): \Generator
{ {
$defs = [];
foreach ($this->getIndexes() as $index) { foreach ($this->getIndexes() as $index) {
foreach ($index->getDefinitions() as $fqn => $def) { yield from $index->getDefinitions();
$defs[$fqn] = $def; }
} }
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getChildDefinitionsForFqn($fqn);
} }
return $defs;
} }
/** /**
@ -132,19 +141,15 @@ abstract class AbstractAggregateIndex implements ReadableIndex
} }
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator providing all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array public function getReferenceUris(string $fqn): \Generator
{ {
$refs = [];
foreach ($this->getIndexes() as $index) { foreach ($this->getIndexes() as $index) {
foreach ($index->getReferenceUris($fqn) as $ref) { yield from $index->getReferenceUris($fqn);
$refs[] = $ref;
}
} }
return $refs;
} }
} }

View File

@ -15,14 +15,26 @@ class Index implements ReadableIndex, \Serializable
use EmitterTrait; use EmitterTrait;
/** /**
* An associative array that maps fully qualified symbol names to Definitions * An associative array that maps splitted fully qualified symbol names
* to definitions, eg :
* [
* 'Psr' => [
* '\Log' => [
* '\LoggerInterface' => [
* '' => $def1, // definition for 'Psr\Log\LoggerInterface' which is non-member
* '->log()' => $def2, // definition for 'Psr\Log\LoggerInterface->log()' which is a member
* ],
* ],
* ],
* ]
* *
* @var Definition[] * @var array
*/ */
private $definitions = []; private $definitions = [];
/** /**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol * An associative array that maps fully qualified symbol names
* to arrays of document URIs that reference the symbol
* *
* @var string[][] * @var string[][]
*/ */
@ -84,14 +96,46 @@ class Index implements ReadableIndex, \Serializable
} }
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definition[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array public function getDefinitions(): \Generator
{ {
return $this->definitions; yield from $this->yieldDefinitionsRecursively($this->definitions);
}
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
$parts = $this->splitFqn($fqn);
if ('' === end($parts)) {
// we want to return all the definitions in the given FQN, not only
// the one (non member) matching exactly the FQN.
array_pop($parts);
}
$result = $this->getIndexValue($parts, $this->definitions);
if (!$result) {
return;
}
foreach ($result as $name => $item) {
// Don't yield the parent
if ($name === '') {
continue;
}
if ($item instanceof Definition) {
yield $fqn.$name => $item;
} elseif (is_array($item) && isset($item[''])) {
yield $fqn.$name => $item[''];
}
}
} }
/** /**
@ -103,12 +147,17 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function getDefinition(string $fqn, bool $globalFallback = false) public function getDefinition(string $fqn, bool $globalFallback = false)
{ {
if (isset($this->definitions[$fqn])) { $parts = $this->splitFqn($fqn);
return $this->definitions[$fqn]; $result = $this->getIndexValue($parts, $this->definitions);
if ($result instanceof Definition) {
return $result;
} }
if ($globalFallback) { if ($globalFallback) {
$parts = explode('\\', $fqn); $parts = explode('\\', $fqn);
$fqn = end($parts); $fqn = end($parts);
return $this->getDefinition($fqn); return $this->getDefinition($fqn);
} }
} }
@ -122,7 +171,9 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function setDefinition(string $fqn, Definition $definition) public function setDefinition(string $fqn, Definition $definition)
{ {
$this->definitions[$fqn] = $definition; $parts = $this->splitFqn($fqn);
$this->indexDefinition(0, $parts, $this->definitions, $definition);
$this->emit('definition-added'); $this->emit('definition-added');
} }
@ -135,19 +186,23 @@ class Index implements ReadableIndex, \Serializable
*/ */
public function removeDefinition(string $fqn) public function removeDefinition(string $fqn)
{ {
unset($this->definitions[$fqn]); $parts = $this->splitFqn($fqn);
$this->removeIndexedDefinition(0, $parts, $this->definitions, $this->definitions);
unset($this->references[$fqn]); unset($this->references[$fqn]);
} }
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator providing all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array public function getReferenceUris(string $fqn): \Generator
{ {
return $this->references[$fqn] ?? []; foreach ($this->references[$fqn] ?? [] as $uri) {
yield $uri;
}
} }
/** /**
@ -204,6 +259,15 @@ class Index implements ReadableIndex, \Serializable
public function unserialize($serialized) public function unserialize($serialized)
{ {
$data = unserialize($serialized); $data = unserialize($serialized);
if (isset($data['definitions'])) {
foreach ($data['definitions'] as $fqn => $definition) {
$this->setDefinition($fqn, $definition);
}
unset($data['definitions']);
}
foreach ($data as $prop => $val) { foreach ($data as $prop => $val) {
$this->$prop = $val; $this->$prop = $val;
} }
@ -215,10 +279,164 @@ class Index implements ReadableIndex, \Serializable
public function serialize() public function serialize()
{ {
return serialize([ return serialize([
'definitions' => $this->definitions, 'definitions' => iterator_to_array($this->getDefinitions()),
'references' => $this->references, 'references' => $this->references,
'complete' => $this->complete, 'complete' => $this->complete,
'staticComplete' => $this->staticComplete 'staticComplete' => $this->staticComplete
]); ]);
} }
/**
* Returns a Generator that yields all the Definitions in the given $storage recursively.
* The generator yields key => value pairs, e.g.
* `'Psr\Log\LoggerInterface->log()' => $definition`
*
* @param array &$storage
* @param string $prefix (optional)
* @return \Generator
*/
private function yieldDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator
{
foreach ($storage as $key => $value) {
if (!is_array($value)) {
yield $prefix.$key => $value;
} else {
yield from $this->yieldDefinitionsRecursively($value, $prefix.$key);
}
}
}
/**
* Splits the given FQN into an array, eg :
* - `'Psr\Log\LoggerInterface->log'` will be `['Psr', '\Log', '\LoggerInterface', '->log()']`
* - `'\Exception->getMessage()'` will be `['\Exception', '->getMessage()']`
* - `'PHP_VERSION'` will be `['PHP_VERSION']`
*
* @param string $fqn
* @return string[]
*/
private function splitFqn(string $fqn): array
{
// split fqn at backslashes
$parts = explode('\\', $fqn);
// write back the backslash prefix to the first part if it was present
if ('' === $parts[0] && count($parts) > 1) {
$parts = array_slice($parts, 1);
$parts[0] = '\\' . $parts[0];
}
// write back the backslashes prefixes for the other parts
for ($i = 1; $i < count($parts); $i++) {
$parts[$i] = '\\' . $parts[$i];
}
// split the last part in 2 parts at the operator
$hasOperator = false;
$lastPart = end($parts);
foreach (['::', '->'] as $operator) {
$endParts = explode($operator, $lastPart);
if (count($endParts) > 1) {
$hasOperator = true;
// replace the last part by its pieces
array_pop($parts);
$parts[] = $endParts[0];
$parts[] = $operator . $endParts[1];
break;
}
}
// The end($parts) === '' holds for the root namespace.
if (!$hasOperator && end($parts) !== '') {
// add an empty part to store the non-member definition to avoid
// definition collisions in the index array, eg
// 'Psr\Log\LoggerInterface' will be stored at
// ['Psr']['\Log']['\LoggerInterface'][''] to be able to also store
// member definitions, ie 'Psr\Log\LoggerInterface->log()' will be
// stored at ['Psr']['\Log']['\LoggerInterface']['->log()']
$parts[] = '';
}
return $parts;
}
/**
* Return the values stored in this index under the given $parts array.
* It can be an index node or a Definition if the $parts are precise
* enough. Returns null when nothing is found.
*
* @param string[] $path The splitted FQN
* @param array|Definition &$storage The current level to look for $path.
* @return array|Definition|null
*/
private function getIndexValue(array $path, &$storage)
{
// Empty path returns the object itself.
if (empty($path)) {
return $storage;
}
$part = array_shift($path);
if (!isset($storage[$part])) {
return null;
}
return $this->getIndexValue($path, $storage[$part]);
}
/**
* Recursive function that stores the given Definition in the given $storage array represented
* as a tree matching the given $parts.
*
* @param int $level The current level of FQN part
* @param string[] $parts The splitted FQN
* @param array &$storage The array in which to store the $definition
* @param Definition $definition The Definition to store
*/
private function indexDefinition(int $level, array $parts, array &$storage, Definition $definition)
{
$part = $parts[$level];
if ($level + 1 === count($parts)) {
$storage[$part] = $definition;
return;
}
if (!isset($storage[$part])) {
$storage[$part] = [];
}
$this->indexDefinition($level + 1, $parts, $storage[$part], $definition);
}
/**
* Recursive function that removes the definition matching the given $parts from the given
* $storage array. The function also looks up recursively to remove the parents of the
* definition which no longer has children to avoid to let empty arrays in the index.
*
* @param int $level The current level of FQN part
* @param string[] $parts The splitted FQN
* @param array &$storage The current array in which to remove data
* @param array &$rootStorage The root storage array
*/
private function removeIndexedDefinition(int $level, array $parts, array &$storage, array &$rootStorage)
{
$part = $parts[$level];
if ($level + 1 === count($parts)) {
if (isset($storage[$part])) {
unset($storage[$part]);
if (0 === count($storage)) {
// parse again the definition tree to remove the parent
// when it has no more children
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
}
}
} else {
$this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage);
}
}
} }

View File

@ -30,12 +30,20 @@ interface ReadableIndex extends EmitterInterface
public function isStaticComplete(): bool; public function isStaticComplete(): bool;
/** /**
* Returns an associative array [string => Definition] that maps fully qualified symbol names * Returns a Generator providing an associative array [string => Definition]
* to Definitions * that maps fully qualified symbol names to Definitions (global or not)
* *
* @return Definitions[] * @return \Generator yields Definition
*/ */
public function getDefinitions(): array; public function getDefinitions(): \Generator;
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator;
/** /**
* Returns the Definition object by a specific FQN * Returns the Definition object by a specific FQN
@ -47,10 +55,10 @@ interface ReadableIndex extends EmitterInterface
public function getDefinition(string $fqn, bool $globalFallback = false); public function getDefinition(string $fqn, bool $globalFallback = false);
/** /**
* Returns all URIs in this index that reference a symbol * Returns a Generator that yields all URIs in this index that reference a symbol
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @return string[] * @return \Generator yields string
*/ */
public function getReferenceUris(string $fqn): array; public function getReferenceUris(string $fqn): \Generator;
} }

View File

@ -6,7 +6,7 @@ namespace LanguageServer;
use LanguageServer\Cache\Cache; use LanguageServer\Cache\Cache;
use LanguageServer\FilesFinder\FilesFinder; use LanguageServer\FilesFinder\FilesFinder;
use LanguageServer\Index\{DependenciesIndex, Index}; use LanguageServer\Index\{DependenciesIndex, Index};
use LanguageServer\Protocol\MessageType; use LanguageServerProtocol\MessageType;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
@ -16,7 +16,7 @@ class Indexer
/** /**
* @var int The prefix for every cache item * @var int The prefix for every cache item
*/ */
const CACHE_VERSION = 2; const CACHE_VERSION = 3;
/** /**
* @var FilesFinder * @var FilesFinder

View File

@ -3,15 +3,15 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
ServerCapabilities, ServerCapabilities,
ClientCapabilities, ClientCapabilities,
TextDocumentSyncKind, TextDocumentSyncKind,
Message,
InitializeResult, InitializeResult,
CompletionOptions, CompletionOptions,
SignatureHelpOptions SignatureHelpOptions
}; };
use LanguageServer\Message;
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 LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
@ -165,8 +165,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* @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 Promise <InitializeResult> * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
{ {
if ($rootPath === null) {
$rootPath = uriToPath($rootUri);
}
return coroutine(function () use ($capabilities, $rootPath, $processId) { return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {

View File

@ -1,9 +1,10 @@
<?php <?php
declare(strict_types = 1); declare(strict_types = 1);
namespace LanguageServer\Protocol; namespace LanguageServer;
use AdvancedJsonRpc\Message as MessageBody; use AdvancedJsonRpc\Message as MessageBody;
use LanguageServer\Message;
class Message class Message
{ {

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
Diagnostic, Position, Range Diagnostic, Position, Range
}; };
use Microsoft\PhpParser; use Microsoft\PhpParser;

View File

@ -1,27 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class ClientCapabilities
{
/**
* The client supports workspace/xfiles requests
*
* @var bool|null
*/
public $xfilesProvider;
/**
* The client supports textDocument/xcontent requests
*
* @var bool|null
*/
public $xcontentProvider;
/**
* The client supports xcache/* requests
*
* @var bool|null
*/
public $xcacheProvider;
}

View File

@ -1,17 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Contains additional diagnostic information about the context in which
* a code action is run.
*/
class CodeActionContext
{
/**
* An array of diagnostics.
*
* @var Diagnostic[]
*/
public $diagnostics;
}

View File

@ -1,35 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A code lens represents a command that should be shown along with
* source text, like the number of references, a way to run tests, etc.
*
* A code lens is _unresolved_ when no command is associated to it. For performance
* reasons the creation of a code lens and resolving should be done in two stages.
*/
class CodeLens
{
/**
* The range in which this code lens is valid. Should only span a single line.
*
* @var Range
*/
public $range;
/**
* The command this code lens represents.
*
* @var Command|null
*/
public $command;
/**
* A data entry field that is preserved on a code lens item between
* a code lens and a code lens resolve request.
*
* @var mixed|null
*/
public $data;
}

View File

@ -1,16 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Code Lens options.
*/
class CodeLensOptions
{
/**
* Code lens has a resolve provider as well.
*
* @var bool|null
*/
public $resolveProvider;
}

View File

@ -1,32 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a reference to a command. Provides a title which will be used to represent a command in the UI and,
* optionally, an array of arguments which will be passed to the command handler function when invoked.
*/
class Command
{
/**
* Title of the command, like `save`.
*
* @var string
*/
public $title;
/**
* The identifier of the actual command handler.
*
* @var string
*/
public $command;
/**
* Arguments that the command handler should be
* invoked with.
*
* @var mixed[]|null
*/
public $arguments;
}

View File

@ -1,30 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Contains additional information about the context in which a completion request is triggered.
*/
class CompletionContext
{
/**
* How the completion was triggered.
*
* @var int
*/
public $triggerKind;
/**
* The trigger character (a single character) that has trigger code complete.
* Is null if `triggerKind !== CompletionTriggerKind::TRIGGER_CHARACTER`
*
* @var string|null
*/
public $triggerCharacter;
public function __construct(int $triggerKind = null, string $triggerCharacter = null)
{
$this->triggerKind = $triggerKind;
$this->triggerCharacter = $triggerCharacter;
}
}

View File

@ -1,164 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
use LanguageServer\Definition;
class CompletionItem
{
/**
* The label of this completion item. By default
* also the text that is inserted when selecting
* this completion.
*
* @var string
*/
public $label;
/**
* The kind of this completion item. Based of the kind
* an icon is chosen by the editor.
*
* @var int|null
*/
public $kind;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*
* @var string|null
*/
public $detail;
/**
* A human-readable string that represents a doc-comment.
*
* @var string|null
*/
public $documentation;
/**
* A string that shoud be used when comparing this item
* with other items. When `falsy` the label is used.
*
* @var string|null
*/
public $sortText;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the label is used.
*
* @var string|null
*/
public $filterText;
/**
* A string that should be inserted a document when selecting
* this completion. When `falsy` the label is used.
*
* @var string|null
*/
public $insertText;
/**
* An edit which is applied to a document when selecting
* this completion. When an edit is provided the value of
* insertText is ignored.
*
* @var TextEdit|null
*/
public $textEdit;
/**
* An optional array of additional text edits that are applied when
* selecting this completion. Edits must not overlap with the main edit
* nor with themselves.
*
* @var TextEdit[]|null
*/
public $additionalTextEdits;
/**
* An optional command that is executed *after* inserting this completion. *Note* that
* additional modifications to the current document should be described with the
* additionalTextEdits-property.
*
* @var Command|null
*/
public $command;
/**
* An data entry field that is preserved on a completion item between
* a completion and a completion resolve request.
*
* @var mixed
*/
public $data;
/**
* @param string $label
* @param int|null $kind
* @param string|null $detail
* @param string|null $documentation
* @param string|null $sortText
* @param string|null $filterText
* @param string|null $insertText
* @param TextEdit|null $textEdit
* @param TextEdit[]|null $additionalTextEdits
* @param Command|null $command
* @param mixed|null $data
*/
public function __construct(
string $label = null,
int $kind = null,
string $detail = null,
string $documentation = null,
string $sortText = null,
string $filterText = null,
string $insertText = null,
TextEdit $textEdit = null,
array $additionalTextEdits = null,
Command $command = null,
$data = null
) {
$this->label = $label;
$this->kind = $kind;
$this->detail = $detail;
$this->documentation = $documentation;
$this->sortText = $sortText;
$this->filterText = $filterText;
$this->insertText = $insertText;
$this->textEdit = $textEdit;
$this->additionalTextEdits = $additionalTextEdits;
$this->command = $command;
$this->data = $data;
}
/**
* Creates a CompletionItem for a Definition
*
* @param Definition $def
* @return self
*/
public static function fromDefinition(Definition $def): self
{
$item = new CompletionItem;
$item->label = $def->symbolInformation->name;
$item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind);
if ($def->type) {
$item->detail = (string)$def->type;
} else if ($def->symbolInformation->containerName) {
$item->detail = $def->symbolInformation->containerName;
}
if ($def->documentation) {
$item->documentation = $def->documentation;
}
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
$item->insertText = '$' . $def->symbolInformation->name;
}
return $item;
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The kind of a completion entry.
*/
abstract class CompletionItemKind
{
const TEXT = 1;
const METHOD = 2;
const FUNCTION = 3;
const CONSTRUCTOR = 4;
const FIELD = 5;
const VARIABLE = 6;
const CLASS_ = 7;
const INTERFACE = 8;
const MODULE = 9;
const PROPERTY = 10;
const UNIT = 11;
const VALUE = 12;
const ENUM = 13;
const KEYWORD = 14;
const SNIPPET = 15;
const COLOR = 16;
const FILE = 17;
const REFERENCE = 18;
/**
* Returns the CompletionItemKind for a SymbolKind
*
* @param int $kind A SymbolKind
* @return int The CompletionItemKind
*/
public static function fromSymbolKind(int $kind): int
{
switch ($kind) {
case SymbolKind::PROPERTY:
case SymbolKind::FIELD:
return self::PROPERTY;
case SymbolKind::METHOD:
return self::METHOD;
case SymbolKind::CLASS_:
return self::CLASS_;
case SymbolKind::INTERFACE:
return self::INTERFACE;
case SymbolKind::FUNCTION:
return self::FUNCTION;
case SymbolKind::NAMESPACE:
case SymbolKind::MODULE:
case SymbolKind::PACKAGE:
return self::MODULE;
case SymbolKind::FILE:
return self::FILE;
case SymbolKind::STRING:
return self::TEXT;
case SymbolKind::NUMBER:
case SymbolKind::BOOLEAN:
case SymbolKind::ARRAY:
return self::VALUE;
case SymbolKind::ENUM:
return self::ENUM;
case SymbolKind::CONSTRUCTOR:
return self::CONSTRUCTOR;
case SymbolKind::VARIABLE:
case SymbolKind::CONSTANT:
return self::VARIABLE;
}
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a collection of completion items to be presented in
* the editor.
*/
class CompletionList
{
/**
* This list it not complete. Further typing should result in recomputing this
* list.
*
* @var bool
*/
public $isIncomplete;
/**
* The completion items.
*
* @var CompletionItem[]
*/
public $items;
/**
* @param CompletionItem[] $items The completion items.
* @param bool $isIncomplete This list it not complete. Further typing should result in recomputing this list.
*/
public function __construct(array $items = [], bool $isIncomplete = false)
{
$this->items = $items;
$this->isIncomplete = $isIncomplete;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Completion options.
*/
class CompletionOptions
{
/*
* The server provides support to resolve additional information for a completion
* item.
*
* @var bool|null
*/
public $resolveProvider;
/**
* The characters that trigger completion automatically.
*
* @var string[]|null
*/
public $triggerCharacters;
}

View File

@ -1,16 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class CompletionTriggerKind
{
/**
* Completion was triggered by invoking it manuall or using API.
*/
const INVOKED = 1;
/**
* Completion was triggered by a trigger character.
*/
const TRIGGER_CHARACTER = 2;
}

View File

@ -1,31 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An event describing a change to a text document. If range and rangeLength are
* omitted the new text is considered to be the full content of the document.
*/
class ContentChangeEvent
{
/**
* The range of the document that changed.
*
* @var Range|null
*/
public $range;
/**
* The length of the range that got replaced.
*
* @var int|null
*/
public $rangeLength;
/**
* The new text of the document.
*
* @var string
*/
public $text;
}

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class DependencyReference
{
/**
* @var mixed
*/
public $hints;
/**
* @var object
*/
public $attributes;
/**
* @param object $attributes
* @param mixed $hints
*/
public function __construct($attributes = null, $hints = null)
{
$this->attributes = $attributes ?? new \stdClass;
$this->hints = $hints;
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a
* resource.
*/
class Diagnostic
{
/**
* The range at which the message applies.
*
* @var Range
*/
public $range;
/**
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*
* @var int|null
*/
public $severity;
/**
* The diagnostic's code. Can be omitted.
*
* @var int|string|null
*/
public $code;
/**
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'.
*
* @var string|null
*/
public $source;
/**
* The diagnostic's message.
*
* @var string
*/
public $message;
/**
* @param string $message The diagnostic's message
* @param Range $range The range at which the message applies
* @param int $code The diagnostic's code
* @param int $severity DiagnosticSeverity
* @param string $source A human-readable string describing the source of this diagnostic
*/
public function __construct(string $message = null, Range $range = null, int $code = null, int $severity = null, string $source = null)
{
$this->message = $message;
$this->range = $range;
$this->code = $code;
$this->severity = $severity;
$this->source = $source;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace LanguageServer\Protocol;
abstract class DiagnosticSeverity
{
/**
* Reports an error.
*/
const ERROR = 1;
/**
* Reports a warning.
*/
const WARNING = 2;
/**
* Reports an information.
*/
const INFORMATION = 3;
/**
* Reports a hint.
*/
const HINT = 4;
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A document highlight is a range inside a text document which deserves
* special attention. Usually a document highlight is visualized by changing
* the background color of its range.
*/
class DocumentHighlight
{
/**
* The range this highlight applies to.
*
* @var Range
*/
public $range;
/**
* The highlight kind, default is DocumentHighlightKind::TEXT.
*
* @var int|null
*/
public $kind;
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A document highlight kind.
*/
abstract class DocumentHighlightKind
{
/**
* A textual occurrance.
*/
const TEXT = 1;
/**
* Read-access of a symbol, like reading a variable.
*/
const READ = 2;
/**
* Write-access of a symbol, like writing to a variable.
*/
const WRITE = 3;
}

View File

@ -1,23 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Format document on type options
*/
class DocumentOnTypeFormattingOptions
{
/**
* A character on which formatting should be triggered, like `}`.
*
* @var string
*/
public $firstTriggerCharacter;
/**
* More trigger characters.
*
* @var string[]|null
*/
public $moreTriggerCharacter;
}

View File

@ -1,17 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Enum
*/
abstract class ErrorCode
{
const PARSE_ERROR = -32700;
const INVALID_REQUEST = -32600;
const METHOD_NOT_FOUND = -32601;
const INVALID_PARAMS = -32602;
const INTERNAL_ERROR = -32603;
const SERVER_ERROR_START = -32099;
const SERVER_ERROR_END = -32000;
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The file event type. Enum
*/
abstract class FileChangeType
{
/**
* The file got created.
*/
const CREATED = 1;
/**
* The file got changed.
*/
const CHANGED = 2;
/**
* The file got deleted.
*/
const DELETED = 3;
}

View File

@ -1,33 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An event describing a file change.
*/
class FileEvent
{
/**
* The file's URI.
*
* @var string
*/
public $uri;
/**
* The change type.
*
* @var int
*/
public $type;
/**
* @param string $uri
* @param int $type
*/
public function __construct(string $uri, int $type)
{
$this->uri = $uri;
$this->type = $type;
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Value-object describing what options formatting should use.
*/
class FormattingOptions
{
/**
* Size of a tab in spaces.
*
* @var int
*/
public $tabSize;
/**
* Prefer spaces over tabs.
*
* @var bool
*/
public $insertSpaces;
// Can be extended with further properties.
}

View File

@ -1,33 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The result of a hover request.
*/
class Hover
{
/**
* The hover's content
*
* @var string|MarkedString|string[]|MarkedString[]
*/
public $contents;
/**
* An optional range
*
* @var Range|null
*/
public $range;
/**
* @param string|MarkedString|string[]|MarkedString[] $contents The hover's content
* @param Range $range An optional range
*/
public function __construct($contents = null, $range = null)
{
$this->contents = $contents;
$this->range = $range;
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class InitializeResult
{
/**
* The capabilities the language server provides.
*
* @var LanguageServer\Protocol\ServerCapabilities
*/
public $capabilities;
/**
* @param LanguageServer\Protocol\ServerCapabilities $capabilities
*/
public function __construct(ServerCapabilities $capabilities = null)
{
$this->capabilities = $capabilities ?? new ServerCapabilities();
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace LanguageServer\Protocol;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/**
* Represents a location inside a resource, such as a line inside a text file.
*/
class Location
{
/**
* @var string
*/
public $uri;
/**
* @var Range
*/
public $range;
/**
* Returns the location of the node
*
* @param Node $node
* @return self
*/
public static function fromNode($node)
{
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
return new self($node->getUri(), new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
));
}
public function __construct(string $uri = null, Range $range = null)
{
$this->uri = $uri;
$this->range = $range;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class MarkedString
{
/**
* @var string
*/
public $language;
/**
* @var string
*/
public $value;
public function __construct(string $language = null, string $value = null)
{
$this->language = $language;
$this->value = $value;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class MessageActionItem
{
/**
* A short title like 'Retry', 'Open Log' etc.
*
* @var string
*/
public $title;
}

View File

@ -1,29 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Enum
*/
abstract class MessageType
{
/**
* An error message.
*/
const ERROR = 1;
/**
* A warning message.
*/
const WARNING = 2;
/**
* An information message.
*/
const INFO = 3;
/**
* A log message.
*/
const LOG = 4;
}

View File

@ -1,25 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
/**
* Uniquely identifies a Composer package
*/
class PackageDescriptor
{
/**
* The package name
*
* @var string
*/
public $name;
/**
* @param string $name The package name
*/
public function __construct(string $name = null)
{
$this->name = $name;
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.
*/
class ParameterInformation
{
/**
* The label of this signature. Will be shown in
* the UI.
*
* @var string
*/
public $label;
/**
* The human-readable doc-comment of this signature. Will be shown
* in the UI but can be omitted.
*
* @var string|null
*/
public $documentation;
/**
* Create ParameterInformation
*
* @param string $label The label of this signature. Will be shown in the UI.
* @param string $documentation The human-readable doc-comment of this signature. Will be shown in the UI but can
* be omitted.
*/
public function __construct(string $label, string $documentation = null)
{
$this->label = $label;
$this->documentation = $documentation;
}
}

View File

@ -1,65 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Position in a text document expressed as zero-based line and character offset.
*/
class Position
{
/**
* Line position in a document (zero-based).
*
* @var int
*/
public $line;
/**
* Character offset on a line in a document (zero-based).
*
* @var int
*/
public $character;
public function __construct(int $line = null, int $character = null)
{
$this->line = $line;
$this->character = $character;
}
/**
* Compares this position to another position
* Returns
* - 0 if the positions match
* - a negative number if $this is before $position
* - a positive number otherwise
*
* @param Position $position
* @return int
*/
public function compare(Position $position): int
{
if ($this->line === $position->line && $this->character === $position->character) {
return 0;
}
if ($this->line !== $position->line) {
return $this->line - $position->line;
}
return $this->character - $position->character;
}
/**
* Returns the offset of the position in a string
*
* @param string $content
* @return int
*/
public function toOffset(string $content): int
{
$lines = explode("\n", $content);
$slice = array_slice($lines, 0, $this->line);
return array_sum(array_map('strlen', $slice)) + count($slice) + $this->character;
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace LanguageServer\Protocol;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/**
* A range in a text document expressed as (zero-based) start and end positions.
*/
class Range
{
/**
* The range's start position.
*
* @var Position
*/
public $start;
/**
* The range's end position.
*
* @var Position
*/
public $end;
/**
* Returns the range the node spans
*
* @param Node $node
* @return self
*/
public static function fromNode(Node $node)
{
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
return new self(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
);
}
public function __construct(Position $start = null, Position $end = null)
{
$this->start = $start;
$this->end = $end;
}
/**
* Checks if a position is within the range
*
* @param Position $position
* @return bool
*/
public function includes(Position $position): bool
{
return $this->start->compare($position) <= 0 && $this->end->compare($position) >= 0;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class ReferenceContext
{
/**
* Include the declaration of the current symbol.
*
* @var bool
*/
public $includeDeclaration;
}

View File

@ -1,36 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*/
class ReferenceInformation
{
/**
* The location in the workspace where the `symbol` is referenced.
*
* @var Location
*/
public $reference;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*
* @var SymbolDescriptor
*/
public $symbol;
/**
* @param Location $reference The location in the workspace where the `symbol` is referenced.
* @param SymbolDescriptor $symbol Metadata about the symbol that can be used to identify or locate its definition.
*/
public function __construct(Location $reference = null, SymbolDescriptor $symbol = null)
{
$this->reference = $reference;
$this->symbol = $symbol;
}
}

View File

@ -1,132 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class ServerCapabilities
{
/**
* Defines how text documents are synced.
*
* @var int|null
*/
public $textDocumentSync;
/**
* The server provides hover support.
*
* @var bool|null
*/
public $hoverProvider;
/**
* The server provides completion support.
*
* @var CompletionOptions|null
*/
public $completionProvider;
/**
* The server provides signature help support.
*
* @var SignatureHelpOptions|null
*/
public $signatureHelpProvider;
/**
* The server provides goto definition support.
*
* @var bool|null
*/
public $definitionProvider;
/**
* The server provides find references support.
*
* @var bool|null
*/
public $referencesProvider;
/**
* The server provides document highlight support.
*
* @var bool|null
*/
public $documentHighlightProvider;
/**
* The server provides document symbol support.
*
* @var bool|null
*/
public $documentSymbolProvider;
/**
* The server provides workspace symbol support.
*
* @var bool|null
*/
public $workspaceSymbolProvider;
/**
* The server provides code actions.
*
* @var bool|null
*/
public $codeActionProvider;
/**
* The server provides code lens.
*
* @var CodeLensOptions|null
*/
public $codeLensProvider;
/**
* The server provides document formatting.
*
* @var bool|null
*/
public $documentFormattingProvider;
/**
* The server provides document range formatting.
*
* @var bool|null
*/
public $documentRangeFormattingProvider;
/**
* The server provides document formatting on typing.
*
* @var DocumentOnTypeFormattingOptions|null
*/
public $documentOnTypeFormattingProvider;
/**
* The server provides rename support.
*
* @var bool|null
*/
public $renameProvider;
/**
* The server provides workspace references exporting support.
*
* @var bool|null
*/
public $xworkspaceReferencesProvider;
/**
* The server provides extended text document definition support.
*
* @var bool|null
*/
public $xdefinitionProvider;
/**
* The server provides workspace dependencies support.
*
* @var bool|null
*/
public $dependenciesProvider;
}

View File

@ -1,46 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Signature help represents the signature of something
* callable. There can be multiple signature but only one
* active and only one active parameter.
*/
class SignatureHelp
{
/**
* One or more signatures.
*
* @var SignatureInformation[]
*/
public $signatures;
/**
* The active signature.
*
* @var int|null
*/
public $activeSignature;
/**
* The active parameter of the active signature.
*
* @var int|null
*/
public $activeParameter;
/**
* Create a SignatureHelp
*
* @param SignatureInformation[] $signatures List of signature information
* @param int|null $activeSignature The active signature, zero based
* @param int|null $activeParameter The active parameter, zero based
*/
public function __construct(array $signatures = [], $activeSignature = null, int $activeParameter = null)
{
$this->signatures = $signatures;
$this->activeSignature = $activeSignature;
$this->activeParameter = $activeParameter;
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Signature help options.
*/
class SignatureHelpOptions
{
/**
* The characters that trigger signature help automatically.
*
* @var string[]|null
*/
public $triggerCharacters;
}

View File

@ -1,49 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents the signature of something callable. A signature
* can have a label, like a function-name, a doc-comment, and
* a set of parameters.
*/
class SignatureInformation
{
/**
* The label of this signature. Will be shown in
* the UI.
*
* @var string
*/
public $label;
/**
* The human-readable doc-comment of this signature. Will be shown
* in the UI but can be omitted.
*
* @var string|null
*/
public $documentation;
/**
* The parameters of this signature.
*
* @var ParameterInformation[]|null
*/
public $parameters;
/**
* Create a SignatureInformation
*
* @param string $label The label of this signature. Will be shown in the UI.
* @param ParameterInformation[]|null The parameters of this signature
* @param string|null The human-readable doc-comment of this signature. Will be shown in the UI
* but can be omitted.
*/
public function __construct(string $label, array $parameters = null, string $documentation = null)
{
$this->label = $label;
$this->parameters = $parameters;
$this->documentation = $documentation;
}
}

View File

@ -1,34 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
/**
* Uniquely identifies a symbol
*/
class SymbolDescriptor
{
/**
* The fully qualified structural element name, a globally unique identifier for the symbol.
*
* @var string
*/
public $fqsen;
/**
* Identifies the Composer package the symbol is defined in (if any)
*
* @var PackageDescriptor|null
*/
public $package;
/**
* @param string $fqsen The fully qualified structural element name, a globally unique identifier for the symbol.
* @param PackageDescriptor $package Identifies the Composer package the symbol is defined in
*/
public function __construct(string $fqsen = null, PackageDescriptor $package = null)
{
$this->fqsen = $fqsen;
$this->package = $package;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A symbol kind.
*/
abstract class SymbolKind
{
const FILE = 1;
const MODULE = 2;
const NAMESPACE = 3;
const PACKAGE = 4;
const CLASS_ = 5;
const METHOD = 6;
const PROPERTY = 7;
const FIELD = 8;
const CONSTRUCTOR = 9;
const ENUM = 10;
const INTERFACE = 11;
const FUNCTION = 12;
const VARIABLE = 13;
const CONSTANT = 14;
const STRING = 15;
const NUMBER = 16;
const BOOLEAN = 17;
const ARRAY = 18;
}

View File

@ -1,32 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class SymbolLocationInformation
{
/**
* The location where the symbol is defined, if any.
*
* @var Location|null
*/
public $location;
/**
* Metadata about the symbol that can be used to identify or locate its
* definition.
*
* @var SymbolDescriptor
*/
public $symbol;
/**
* @param SymbolDescriptor $symbol The location where the symbol is defined, if any
* @param Location $location Metadata about the symbol that can be used to identify or locate its definition
*/
public function __construct(SymbolDescriptor $symbol = null, Location $location = null)
{
$this->symbol = $symbol;
$this->location = $location;
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An event describing a change to a text document. If range and rangeLength are omitted
* the new text is considered to be the full content of the document.
*/
class TextDocumentContentChangeEvent
{
/**
* The range of the document that changed.
*
* @var Range|null
*/
public $range;
/**
* The length of the range that got replaced.
*
* @var int|null
*/
public $rangeLength;
/**
* The new text of the document.
*
* @var string
*/
public $text;
}

View File

@ -1,21 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class TextDocumentIdentifier
{
/**
* The text document's URI.
*
* @var string
*/
public $uri;
/**
* @param string $uri The text document's URI.
*/
public function __construct(string $uri = null)
{
$this->uri = $uri;
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An item to transfer a text document from the client to the server.
*/
class TextDocumentItem
{
/**
* The text document's URI.
*
* @var string
*/
public $uri;
/**
* The text document's language identifier.
*
* @var string
*/
public $languageId;
/**
* The version number of this document (it will strictly increase after each
* change, including undo/redo).
*
* @var int
*/
public $version;
/**
* The content of the opened text document.
*
* @var string
*/
public $text;
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Defines how the host (editor) should sync document changes to the language server.
*/
abstract class TextDocumentSyncKind
{
/**
* Documents should not be synced at all.
*/
const NONE = 0;
/**
* Documents are synced by always sending the full content of the document.
*/
const FULL = 1;
/*
* Documents are synced by sending the full content on open. After that only
* incremental updates to the document are sent.
*/
const INCREMENTAL = 2;
}

View File

@ -1,31 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A textual edit applicable to a text document.
*/
class TextEdit
{
/**
* The range of the text document to be manipulated. To insert
* text into a document create a range where start === end.
*
* @var Range
*/
public $range;
/**
* The string to be inserted. For delete operations use an
* empty string.
*
* @var string
*/
public $newText;
public function __construct(Range $range = null, string $newText = null)
{
$this->range = $range;
$this->newText = $newText;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class VersionedTextDocumentIdentifier extends TextDocumentIdentifier
{
/**
* The version number of this document.
*
* @var int
*/
public $version;
}

View File

@ -1,16 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A workspace edit represents changes to many resources managed in the workspace.
*/
class WorkspaceEdit
{
/**
* Holds changes to existing resources. Associative Array from URI to TextEdit
*
* @var TextEdit[]
*/
public $changes;
}

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use AdvancedJsonRpc\Message as MessageBody; use AdvancedJsonRpc\Message as MessageBody;
use Sabre\Event\{Loop, Emitter}; use Sabre\Event\{Loop, Emitter};

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use Sabre\Event\{ use Sabre\Event\{
Loop, Loop,
Promise Promise

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use Sabre\Event\Promise; use Sabre\Event\Promise;
interface ProtocolWriter interface ProtocolWriter

View File

@ -7,7 +7,9 @@ use LanguageServer\{
CompletionProvider, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver CompletionProvider, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
}; };
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServer\Factory\LocationFactory;
use LanguageServer\Factory\RangeFactory;
use LanguageServerProtocol\{
FormattingOptions, FormattingOptions,
Hover, Hover,
Location, Location,
@ -108,7 +110,7 @@ class TextDocument
* The document symbol request is sent from the client to the server to list all symbols found in a given text * The document symbol request is sent from the client to the server to list all symbols found in a given text
* document. * document.
* *
* @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument * @param \LanguageServerProtocol\TextDocumentIdentifier $textDocument
* @return Promise <SymbolInformation[]> * @return Promise <SymbolInformation[]>
*/ */
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
@ -127,7 +129,7 @@ class TextDocument
* document's truth is now managed by the client and the server must not try to read the document's truth using the * document's truth is now managed by the client and the server must not try to read the document's truth using the
* document's uri. * document's uri.
* *
* @param \LanguageServer\Protocol\TextDocumentItem $textDocument The document that was opened. * @param \LanguageServerProtocol\TextDocumentItem $textDocument The document that was opened.
* @return void * @return void
*/ */
public function didOpen(TextDocumentItem $textDocument) public function didOpen(TextDocumentItem $textDocument)
@ -141,8 +143,8 @@ class TextDocument
/** /**
* The document change notification is sent from the client to the server to signal changes to a text document. * The document change notification is sent from the client to the server to signal changes to a text document.
* *
* @param \LanguageServer\Protocol\VersionedTextDocumentIdentifier $textDocument * @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument
* @param \LanguageServer\Protocol\TextDocumentContentChangeEvent[] $contentChanges * @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges
* @return void * @return void
*/ */
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
@ -157,7 +159,7 @@ class TextDocument
* The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the * The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the
* truth now exists on disk). * truth now exists on disk).
* *
* @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument The document that was closed * @param \LanguageServerProtocol\TextDocumentIdentifier $textDocument The document that was closed
* @return void * @return void
*/ */
public function didClose(TextDocumentIdentifier $textDocument) public function didClose(TextDocumentIdentifier $textDocument)
@ -208,7 +210,7 @@ class TextDocument
if ($descendantNode instanceof Node\Expression\Variable && if ($descendantNode instanceof Node\Expression\Variable &&
$descendantNode->getName() === $node->getName() $descendantNode->getName() === $node->getName()
) { ) {
$locations[] = Location::fromNode($descendantNode); $locations[] = LocationFactory::fromNode($descendantNode);
} }
} }
} else { } else {
@ -225,15 +227,16 @@ class TextDocument
return []; return [];
} }
} }
$refDocuments = yield Promise\all(array_map( $refDocumentPromises = [];
[$this->documentLoader, 'getOrLoad'], foreach ($this->index->getReferenceUris($fqn) as $uri) {
$this->index->getReferenceUris($fqn) $refDocumentPromises[] = $this->documentLoader->getOrLoad($uri);
)); }
$refDocuments = yield Promise\all($refDocumentPromises);
foreach ($refDocuments as $document) { foreach ($refDocuments as $document) {
$refs = $document->getReferenceNodesByFqn($fqn); $refs = $document->getReferenceNodesByFqn($fqn);
if ($refs !== null) { if ($refs !== null) {
foreach ($refs as $ref) { foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref); $locations[] = LocationFactory::fromNode($ref);
} }
} }
} }
@ -332,7 +335,7 @@ class TextDocument
} }
yield waitForEvent($this->index, 'definition-added'); yield waitForEvent($this->index, 'definition-added');
} }
$range = Range::fromNode($node); $range = RangeFactory::fromNode($node);
if ($def === null) { if ($def === null) {
return new Hover([], $range); return new Hover([], $range);
} }

View File

@ -5,7 +5,8 @@ namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, PhpDocumentLoader}; use LanguageServer\{LanguageClient, PhpDocumentLoader};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{ use LanguageServer\Factory\LocationFactory;
use LanguageServerProtocol\{
FileChangeType, FileChangeType,
FileEvent, FileEvent,
SymbolInformation, SymbolInformation,
@ -150,7 +151,7 @@ class Workspace
$doc = yield $this->documentLoader->getOrLoad($uri); $doc = yield $this->documentLoader->getOrLoad($uri);
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) { foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
$refInfo = new ReferenceInformation; $refInfo = new ReferenceInformation;
$refInfo->reference = Location::fromNode($node); $refInfo->reference = LocationFactory::fromNode($node);
$refInfo->symbol = $query; $refInfo->symbol = $query;
$refInfos[] = $refInfo; $refInfos[] = $refInfo;
} }

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
Position, Position,
SignatureHelp SignatureHelp
}; };

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{SignatureInformation, ParameterInformation}; use LanguageServerProtocol\{SignatureInformation, ParameterInformation};
use Microsoft\PhpParser\FunctionLike; use Microsoft\PhpParser\FunctionLike;
class SignatureInformationFactory class SignatureInformationFactory

View File

@ -3,7 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position}; use LanguageServer\Factory\RangeFactory;
use LanguageServerProtocol\{Diagnostic, DiagnosticSeverity, Range, Position};
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Microsoft\PhpParser; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node; use Microsoft\PhpParser\Node;
@ -100,7 +101,7 @@ class TreeAnalyzer
if ($method && $method->isStatic()) { if ($method && $method->isStatic()) {
$this->diagnostics[] = new Diagnostic( $this->diagnostics[] = new Diagnostic(
"\$this can not be used in static methods.", "\$this can not be used in static methods.",
Range::fromNode($node), RangeFactory::fromNode($node),
null, null,
DiagnosticSeverity::ERROR, DiagnosticSeverity::ERROR,
'php' 'php'

View File

@ -38,7 +38,7 @@ function pathToUri(string $filepath): string
function uriToPath(string $uri) function uriToPath(string $uri)
{ {
$fragments = parse_url($uri); $fragments = parse_url($uri);
if ($fragments === null || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') { if ($fragments === false || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') {
throw new InvalidArgumentException("Not a valid file URI: $uri"); throw new InvalidArgumentException("Not a valid file URI: $uri");
} }
$filepath = urldecode($fragments['path']); $filepath = urldecode($fragments['path']);

View File

@ -5,7 +5,7 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\Loop; use Sabre\Event\Loop;

View File

@ -9,7 +9,7 @@ use LanguageServer\{
DefinitionResolver, TreeAnalyzer DefinitionResolver, TreeAnalyzer
}; };
use LanguageServer\Index\{Index}; use LanguageServer\Index\{Index};
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
Diagnostic, DiagnosticSeverity, Position, Range Diagnostic, DiagnosticSeverity, Position, Range
}; };
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;

View File

@ -5,8 +5,8 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer; use LanguageServer\LanguageServer;
use LanguageServer\Protocol\{ use LanguageServer\Message;
Message, use LanguageServerProtocol\{
ClientCapabilities, ClientCapabilities,
TextDocumentSyncKind, TextDocumentSyncKind,
MessageType, MessageType,

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Tests; namespace LanguageServer\Tests;
use LanguageServer\{ProtocolReader, ProtocolWriter}; use LanguageServer\{ProtocolReader, ProtocolWriter};
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use Sabre\Event\{Loop, Emitter, Promise}; use Sabre\Event\{Loop, Emitter, Promise};
/** /**

View File

@ -35,7 +35,9 @@ class DefinitionCollectorTest extends TestCase
'TestNamespace\\ChildClass', 'TestNamespace\\ChildClass',
'TestNamespace\\Example', 'TestNamespace\\Example',
'TestNamespace\\Example->__construct()', 'TestNamespace\\Example->__construct()',
'TestNamespace\\Example->__destruct()' 'TestNamespace\\Example->__destruct()',
'TestNamespace\\InnerNamespace',
'TestNamespace\\InnerNamespace\\InnerClass',
], array_keys($defNodes)); ], array_keys($defNodes));
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']); $this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']);
@ -53,6 +55,7 @@ class DefinitionCollectorTest extends TestCase
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']); $this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\InnerNamespace\\InnerClass']);
} }
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()

View File

@ -9,7 +9,7 @@ use LanguageServer\{
use LanguageServer\Index\{ use LanguageServer\Index\{
Index Index
}; };
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
Position Position
}; };
use Microsoft\PhpParser; use Microsoft\PhpParser;

View File

@ -5,7 +5,7 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter}; use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use Sabre\Event\Loop; use Sabre\Event\Loop;

View File

@ -5,7 +5,7 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\ProtocolStreamWriter; use LanguageServer\ProtocolStreamWriter;
use LanguageServer\Protocol\Message; use LanguageServer\Message;
use AdvancedJsonRpc\{Request as RequestBody}; use AdvancedJsonRpc\{Request as RequestBody};
use Sabre\Event\Loop; use Sabre\Event\Loop;

View File

@ -10,7 +10,7 @@ use LanguageServer\{
}; };
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range}; use LanguageServerProtocol\{Position, Location, Range};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
abstract class ServerTestCase extends TestCase abstract class ServerTestCase extends TestCase
@ -107,7 +107,9 @@ abstract class ServerTestCase extends TestCase
'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))), 'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))),
'TestNamespace\\Example' => new Location($symbolsUri, new Range(new Position(101, 0), new Position(104, 1))), 'TestNamespace\\Example' => new Location($symbolsUri, new Range(new Position(101, 0), new Position(104, 1))),
'TestNamespace\\Example::__construct' => new Location($symbolsUri, new Range(new Position(102, 4), new Position(102, 36))), 'TestNamespace\\Example::__construct' => new Location($symbolsUri, new Range(new Position(102, 4), new Position(102, 36))),
'TestNamespace\\Example::__destruct' => new Location($symbolsUri, new Range(new Position(103, 4), new Position(103, 35))) 'TestNamespace\\Example::__destruct' => new Location($symbolsUri, new Range(new Position(103, 4), new Position(103, 35))),
'TestNamespace\\InnerNamespace' => new Location($symbolsUri, new Range(new Position(106, 0), new Position(106, 39))),
'TestNamespace\\InnerNamespace\\InnerClass' => new Location($symbolsUri, new Range(new Position(108, 0), new Position(109, 1))),
]; ];
$this->referenceLocations = [ $this->referenceLocations = [

View File

@ -10,7 +10,7 @@ use LanguageServer\{
}; };
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextEdit, TextEdit,
Range, Range,
@ -47,6 +47,9 @@ class CompletionTest extends TestCase
$this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex);
} }
/**
* Tests completion at `$obj->t|`
*/
public function testPropertyAndMethodWithPrefix() public function testPropertyAndMethodWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php');
@ -71,6 +74,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `public function a() { tes| }`
*/
public function testGlobalFunctionInsideNamespaceAndClass() public function testGlobalFunctionInsideNamespaceAndClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php');
@ -92,6 +98,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$obj->|`
*/
public function testPropertyAndMethodWithoutPrefix() public function testPropertyAndMethodWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
@ -116,6 +125,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$|` when variables are defined
*/
public function testVariable() public function testVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php');
@ -148,6 +160,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$p|` when variables are defined
*/
public function testVariableWithPrefix() public function testVariableWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php');
@ -170,6 +185,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `new|` when in a namespace and have used variables.
*/
public function testNewInNamespace() public function testNewInNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php');
@ -218,27 +236,12 @@ class CompletionTest extends TestCase
null, null,
'TestClass' 'TestClass'
), ),
new CompletionItem(
'ChildClass',
CompletionItemKind::CLASS_,
'TestNamespace',
null,
null,
null,
'\TestNamespace\ChildClass'
),
new CompletionItem(
'Example',
CompletionItemKind::CLASS_,
'TestNamespace',
null,
null,
null,
'\TestNamespace\Example'
)
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestC|` with `use TestNamespace\TestClass`
*/
public function testUsedClass() public function testUsedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php');
@ -257,11 +260,74 @@ class CompletionTest extends TestCase
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" . 'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" . 'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" . 'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.' 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null,
null,
'TestClass'
) )
], true), $items); ], true), $items);
$this->assertCompletionsListDoesNotContainLabel('OtherClass', $items);
$this->assertCompletionsListDoesNotContainLabel('TestInterface', $items);
} }
/**
* Tests completion at `AliasNamespace\I|` with `use TestNamespace\InnerNamespace as AliasNamespace`
*/
public function testUsedNamespaceWithPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(8, 16)
)->wait();
$this->assertEquals(
new CompletionList([
new CompletionItem(
'InnerClass',
CompletionItemKind::CLASS_,
'TestNamespace\\InnerNamespace',
null,
null,
null,
'AliasNamespace\\InnerClass'
)
], true),
$items
);
}
/**
* Tests completion at `AliasNamespace\|` with `use TestNamespace\InnerNamespace as AliasNamespace`
*/
public function testUsedNamespaceWithoutPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(9, 15)
)->wait();
$this->assertEquals(
new CompletionList([
new CompletionItem(
'InnerClass',
CompletionItemKind::CLASS_,
'TestNamespace\InnerNamespace',
null,
null,
null,
'AliasNamespace\InnerClass'
),
], true),
$items
);
}
/**
* Tests completion at `TestClass::$st|`
*/
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');
@ -283,6 +349,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::|`
*/
public function testStaticWithoutPrefix() public function testStaticWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php');
@ -316,6 +385,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::st|`
*/
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');
@ -325,21 +397,6 @@ class CompletionTest extends TestCase
new Position(2, 13) new Position(2, 13)
)->wait(); )->wait();
$this->assertCompletionsListSubset(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE,
'int',
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
),
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
'Lorem excepteur officia sit anim velit veniam enim.',
null,
null,
'$staticTestProperty'
),
new CompletionItem( new CompletionItem(
'staticTestMethod', 'staticTestMethod',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
@ -349,6 +406,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `TestClass::TE` at the root level.
*/
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');
@ -363,25 +423,13 @@ class CompletionTest extends TestCase
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
'int', 'int',
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
),
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
'Lorem excepteur officia sit anim velit veniam enim.',
null,
null,
'$staticTestProperty'
),
new CompletionItem(
'staticTestMethod',
CompletionItemKind::METHOD,
'mixed',
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
) )
], true), $items); ], true), $items);
} }
/**
* Test completion at `\TestC|` in a namespace
*/
public function testFullyQualifiedClass() public function testFullyQualifiedClass()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php');
@ -400,14 +448,18 @@ class CompletionTest extends TestCase
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" . 'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" . 'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" . 'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.', 'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.'
null,
null,
'TestClass'
) )
], true), $items); ], true), $items);
// Assert that all results are non-namespaced.
foreach ($items->items as $item) {
$this->assertSame($item->detail, null);
}
} }
/**
* Tests completion at `cl|` at root level
*/
public function testKeywords() public function testKeywords()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
@ -422,6 +474,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion in an empty file
*/
public function testHtmlWithoutPrefix() public function testHtmlWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
@ -444,6 +499,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion in `<|` when not within `<?php` tags
*/
public function testHtmlWontBeProposedWithoutCompletionContext() public function testHtmlWontBeProposedWithoutCompletionContext()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
@ -456,6 +514,9 @@ class CompletionTest extends TestCase
$this->assertEquals(new CompletionList([], true), $items); $this->assertEquals(new CompletionList([], true), $items);
} }
/**
* Tests completion in `<|` when not within `<?php` tags
*/
public function testHtmlWontBeProposedWithPrefixWithCompletionContext() public function testHtmlWontBeProposedWithPrefixWithCompletionContext()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
@ -480,6 +541,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `<|` when not within `<?php` tags when triggered by trigger character.
*/
public function testHtmlPrefixShouldNotTriggerCompletion() public function testHtmlPrefixShouldNotTriggerCompletion()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
@ -492,6 +556,9 @@ class CompletionTest extends TestCase
$this->assertEquals(new CompletionList([], true), $items); $this->assertEquals(new CompletionList([], true), $items);
} }
/**
* Tests completion at `<|` when not within `<?php` tags when triggered by user input.
*/
public function testHtmlPrefixShouldTriggerCompletionIfManuallyInvoked() public function testHtmlPrefixShouldTriggerCompletionIfManuallyInvoked()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
@ -515,6 +582,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `SomeNa|` when namespace `SomeNamespace` is defined
*/
public function testNamespace() public function testNamespace()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php');
@ -526,17 +596,15 @@ class CompletionTest extends TestCase
$this->assertCompletionsListSubset(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'SomeNamespace', 'SomeNamespace',
CompletionItemKind::MODULE, CompletionItemKind::MODULE
null,
null,
null,
null,
'SomeNamespace'
) )
], true), $items); ], true), $items);
} }
public function testBarePhp() /**
* Tests completion at `echo $ab|` at the root level.
*/
public function testBarePhpVariable()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/bare_php.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/bare_php.php');
$this->loader->open($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
@ -776,6 +844,16 @@ class CompletionTest extends TestCase
$this->assertEquals($subsetList->isIncomplete, $list->isIncomplete); $this->assertEquals($subsetList->isIncomplete, $list->isIncomplete);
} }
private function assertCompletionsListDoesNotContainLabel(string $label, CompletionList $list)
{
foreach ($list->items as $item) {
$this->assertNotSame($label, $item->label, "Completion list should not contain $label.");
}
}
/**
* Tests completion for `$this->|`
*/
public function testThisWithoutPrefix() public function testThisWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php');
@ -812,6 +890,9 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$this->m|`
*/
public function testThisWithPrefix() public function testThisWithPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php');
@ -821,18 +902,6 @@ class CompletionTest extends TestCase
new Position(12, 16) new Position(12, 16)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertEquals(new CompletionList([
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
'\TestClass', // Type of the property
'Reprehenderit magna velit mollit ipsum do.'
),
new CompletionItem(
'testMethod',
CompletionItemKind::METHOD,
'\TestClass', // Return type of the method
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
),
new CompletionItem( new CompletionItem(
'foo', 'foo',
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
@ -856,10 +925,25 @@ class CompletionTest extends TestCase
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
'mixed', // Return type of the method 'mixed', // Return type of the method
null null
) ),
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
'\TestClass', // Type of the property
'Reprehenderit magna velit mollit ipsum do.'
),
new CompletionItem(
'testMethod',
CompletionItemKind::METHOD,
'\TestClass', // Return type of the method
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
),
], true), $items); ], true), $items);
} }
/**
* Tests completion at `$this->foo()->q|`
*/
public function testThisReturnValue() public function testThisReturnValue()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php');
@ -869,11 +953,6 @@ class CompletionTest extends TestCase
new Position(17, 23) new Position(17, 23)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertEquals(new CompletionList([
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'$this' // Return type of the method
),
new CompletionItem( new CompletionItem(
'bar', 'bar',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
@ -883,7 +962,12 @@ class CompletionTest extends TestCase
'qux', 'qux',
CompletionItemKind::METHOD, CompletionItemKind::METHOD,
'mixed' // Return type of the method 'mixed' // Return type of the method
) ),
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'$this' // Return type of the method
),
], true), $items); ], true), $items);
} }
} }

View File

@ -10,7 +10,7 @@ use LanguageServer\{
}; };
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location}; use LanguageServerProtocol\{TextDocumentIdentifier, Position, Range, Location};
class GlobalFallbackTest extends ServerTestCase class GlobalFallbackTest extends ServerTestCase
{ {

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument\Definition; namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Location, Range}; use LanguageServerProtocol\{TextDocumentIdentifier, Position, Location, Range};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
class GlobalTest extends ServerTestCase class GlobalTest extends ServerTestCase

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument\Definition; namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Protocol\{TextDocumentIdentifier, Location}; use LanguageServerProtocol\{TextDocumentIdentifier, Location};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
class NamespacedTest extends GlobalTest class NamespacedTest extends GlobalTest

View File

@ -10,7 +10,7 @@ use LanguageServer\{
}; };
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{ use LanguageServerProtocol\{
VersionedTextDocumentIdentifier, VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent, TextDocumentContentChangeEvent,
Range, Range,

View File

@ -10,7 +10,7 @@ use LanguageServer\{
}; };
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier}; use LanguageServerProtocol\{TextDocumentItem, TextDocumentIdentifier};
class DidCloseTest extends TestCase class DidCloseTest extends TestCase
{ {

View File

@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument;
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project}; use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, SymbolInformation, SymbolKind, Position, Location, Range}; use LanguageServerProtocol\{TextDocumentIdentifier, SymbolInformation, SymbolKind, Position, Location, Range};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
class DocumentSymbolTest extends ServerTestCase class DocumentSymbolTest extends ServerTestCase
@ -32,7 +32,9 @@ class DocumentSymbolTest extends ServerTestCase
new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'),
new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'), new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'),
new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'), new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'),
new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example') new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example'),
new SymbolInformation('TestNamespace\\InnerNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace\\InnerNamespace'), 'TestNamespace'),
new SymbolInformation('InnerClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\InnerNamespace\\InnerClass'), 'TestNamespace\\InnerNamespace'),
], $result); ], $result);
// @codingStandardsIgnoreEnd // @codingStandardsIgnoreEnd
} }

Some files were not shown because too many files have changed in this diff Show More