Completion (#165)
* Add support for method/property completion * Move completion fixtures into directory * Add support for variable suggestions Refactor logic into CompletionProvider class * Allow getTypeFromNode() to take Variable nodes * Use property and constant values * Fix using @var tag for variables * Improve completion * classes * variables with prefix filtering * Make FQNs more distinct * use -> for instance methods/properties * use ::$ for static properties * Add tests for static access * Properly filter completion on empty property * Fix existing tests * Add support for static access without prefix * Fix testFullyQualifiedClass * Add missing fixtures * Correct file number in init test * Only insert backslash if not typed yet * Completion for keywords and bug fixes * Correct variable insertion * Support completion for namespaces * Use CompletionList * Always set isIncomplete to true * Update PHPCodeSniffer * Remove unused method * And the call * Handle case where FQN could not be resolvedpull/181/merge
parent
f56b14438b
commit
10fb3c92e0
|
@ -13,5 +13,5 @@ indent_size = 2
|
||||||
[composer.json]
|
[composer.json]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
[*.md]
|
[{*.md,fixtures/**}]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
|
@ -24,11 +24,11 @@
|
||||||
"bin": ["bin/php-language-server.php"],
|
"bin": ["bin/php-language-server.php"],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0",
|
"php": ">=7.0",
|
||||||
"nikic/php-parser": "^3.0.0beta2",
|
"nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c",
|
||||||
"phpdocumentor/reflection-docblock": "^3.0",
|
"phpdocumentor/reflection-docblock": "^3.0",
|
||||||
"sabre/event": "^5.0",
|
"sabre/event": "^5.0",
|
||||||
"felixfbecker/advanced-json-rpc": "^2.0",
|
"felixfbecker/advanced-json-rpc": "^2.0",
|
||||||
"squizlabs/php_codesniffer" : "3.0.0RC1",
|
"squizlabs/php_codesniffer" : "3.0.x-dev#e8acf8e029301b0e3ea7e7c9eef0aee914db78bf",
|
||||||
"netresearch/jsonmapper": "^1.0",
|
"netresearch/jsonmapper": "^1.0",
|
||||||
"webmozart/path-util": "^2.3",
|
"webmozart/path-util": "^2.3",
|
||||||
"webmozart/glob": "^4.1",
|
"webmozart/glob": "^4.1",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
TestClass::TE
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Whatever;
|
||||||
|
|
||||||
|
use TestNamespace\{TestClass, TestInterface};
|
||||||
|
|
||||||
|
\TestC
|
||||||
|
|
||||||
|
class OtherClass {}
|
|
@ -0,0 +1 @@
|
||||||
|
<
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
cl
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SomeNamespace {}
|
||||||
|
|
||||||
|
SomeNa
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$obj = new TestClass;
|
||||||
|
$obj->
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$obj = new TestClass;
|
||||||
|
$obj->t
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
TestClass::
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
TestClass::st
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
TestClass::$st
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Whatever;
|
||||||
|
|
||||||
|
use TestNamespace\{TestClass, TestInterface};
|
||||||
|
|
||||||
|
TestC
|
||||||
|
|
||||||
|
class OtherClass {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Whatever;
|
||||||
|
|
||||||
|
use TestNamespace\{TestClass, TestInterface};
|
||||||
|
|
||||||
|
$obj = new
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $param A parameter
|
||||||
|
*/
|
||||||
|
function test(string $param = null)
|
||||||
|
{
|
||||||
|
$var = 123;
|
||||||
|
$
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|null $param A parameter
|
||||||
|
*/
|
||||||
|
function test(string $param = null)
|
||||||
|
{
|
||||||
|
$var = 123;
|
||||||
|
$p
|
||||||
|
}
|
|
@ -0,0 +1,385 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use phpDocumentor\Reflection\Types;
|
||||||
|
use LanguageServer\Protocol\{
|
||||||
|
TextEdit,
|
||||||
|
Range,
|
||||||
|
Position,
|
||||||
|
SymbolKind,
|
||||||
|
CompletionList,
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind
|
||||||
|
};
|
||||||
|
|
||||||
|
class CompletionProvider
|
||||||
|
{
|
||||||
|
const KEYWORDS = [
|
||||||
|
'?>',
|
||||||
|
'__halt_compiler',
|
||||||
|
'abstract',
|
||||||
|
'and',
|
||||||
|
'array',
|
||||||
|
'as',
|
||||||
|
'break',
|
||||||
|
'callable',
|
||||||
|
'case',
|
||||||
|
'catch',
|
||||||
|
'class',
|
||||||
|
'clone',
|
||||||
|
'const',
|
||||||
|
'continue',
|
||||||
|
'declare',
|
||||||
|
'default',
|
||||||
|
'die',
|
||||||
|
'do',
|
||||||
|
'echo',
|
||||||
|
'else',
|
||||||
|
'elseif',
|
||||||
|
'empty',
|
||||||
|
'enddeclare',
|
||||||
|
'endfor',
|
||||||
|
'endforeach',
|
||||||
|
'endif',
|
||||||
|
'endswitch',
|
||||||
|
'endwhile',
|
||||||
|
'eval',
|
||||||
|
'exit',
|
||||||
|
'extends',
|
||||||
|
'final',
|
||||||
|
'finally',
|
||||||
|
'for',
|
||||||
|
'foreach',
|
||||||
|
'function',
|
||||||
|
'global',
|
||||||
|
'goto',
|
||||||
|
'if',
|
||||||
|
'implements',
|
||||||
|
'include',
|
||||||
|
'include_once',
|
||||||
|
'instanceof',
|
||||||
|
'insteadof',
|
||||||
|
'interface',
|
||||||
|
'isset',
|
||||||
|
'list',
|
||||||
|
'namespace',
|
||||||
|
'new',
|
||||||
|
'or',
|
||||||
|
'print',
|
||||||
|
'private',
|
||||||
|
'protected',
|
||||||
|
'public',
|
||||||
|
'require',
|
||||||
|
'require_once',
|
||||||
|
'return',
|
||||||
|
'static',
|
||||||
|
'switch',
|
||||||
|
'throw',
|
||||||
|
'trait',
|
||||||
|
'try',
|
||||||
|
'unset',
|
||||||
|
'use',
|
||||||
|
'var',
|
||||||
|
'while',
|
||||||
|
'xor',
|
||||||
|
'yield'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DefinitionResolver
|
||||||
|
*/
|
||||||
|
private $definitionResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Project
|
||||||
|
*/
|
||||||
|
private $project;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DefinitionResolver $definitionResolver
|
||||||
|
* @param Project $project
|
||||||
|
*/
|
||||||
|
public function __construct(DefinitionResolver $definitionResolver, Project $project)
|
||||||
|
{
|
||||||
|
$this->definitionResolver = $definitionResolver;
|
||||||
|
$this->project = $project;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns suggestions for a specific cursor position in a document
|
||||||
|
*
|
||||||
|
* @param PhpDocument $doc The opened document
|
||||||
|
* @param Position $pos The cursor position
|
||||||
|
* @return CompletionList
|
||||||
|
*/
|
||||||
|
public function provideCompletion(PhpDocument $doc, Position $pos): CompletionList
|
||||||
|
{
|
||||||
|
$node = $doc->getNodeAtPosition($pos);
|
||||||
|
|
||||||
|
if ($node instanceof Node\Expr\Error) {
|
||||||
|
$node = $node->getAttribute('parentNode');
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = new CompletionList;
|
||||||
|
$list->isIncomplete = true;
|
||||||
|
|
||||||
|
// A non-free node means we do NOT suggest global symbols
|
||||||
|
if (
|
||||||
|
$node instanceof Node\Expr\MethodCall
|
||||||
|
|| $node instanceof Node\Expr\PropertyFetch
|
||||||
|
|| $node instanceof Node\Expr\StaticCall
|
||||||
|
|| $node instanceof Node\Expr\StaticPropertyFetch
|
||||||
|
|| $node instanceof Node\Expr\ClassConstFetch
|
||||||
|
) {
|
||||||
|
if (!is_string($node->name)) {
|
||||||
|
// If the name is an Error node, just filter by the class
|
||||||
|
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
|
||||||
|
// For instances, resolve the variable type
|
||||||
|
$prefixes = DefinitionResolver::getFqnsFromType(
|
||||||
|
$this->definitionResolver->resolveExpressionNodeToType($node->var)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$prefixes = [$node->class instanceof Node\Name ? (string)$node->class : ''];
|
||||||
|
}
|
||||||
|
// If we are just filtering by the class, add the appropiate operator to the prefix
|
||||||
|
// to filter the type of symbol
|
||||||
|
foreach ($prefixes as &$prefix) {
|
||||||
|
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
|
||||||
|
$prefix .= '->';
|
||||||
|
} else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) {
|
||||||
|
$prefix .= '::';
|
||||||
|
} else if ($node instanceof Node\Expr\StaticPropertyFetch) {
|
||||||
|
$prefix .= '::$';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
|
||||||
|
$prefixes = $fqn !== null ? [$fqn] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->project->getDefinitions() as $fqn => $def) {
|
||||||
|
foreach ($prefixes as $prefix) {
|
||||||
|
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
|
||||||
|
$list->items[] = CompletionItem::fromDefinition($def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
// A ConstFetch means any static reference, like a class, interface, etc. or keyword
|
||||||
|
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|
||||||
|
|| $node instanceof Node\Expr\New_
|
||||||
|
) {
|
||||||
|
$prefix = '';
|
||||||
|
$prefixLen = 0;
|
||||||
|
if ($node instanceof Node\Name) {
|
||||||
|
$isFullyQualified = $node->isFullyQualified();
|
||||||
|
$prefix = (string)$node;
|
||||||
|
$prefixLen = strlen($prefix);
|
||||||
|
$namespacedPrefix = (string)$node->getAttribute('namespacedName');
|
||||||
|
$namespacedPrefixLen = strlen($prefix);
|
||||||
|
}
|
||||||
|
// Find closest namespace
|
||||||
|
$namespace = getClosestNode($node, Node\Stmt\Namespace_::class);
|
||||||
|
/** Map from alias to Definition */
|
||||||
|
$aliasedDefs = [];
|
||||||
|
if ($namespace) {
|
||||||
|
foreach ($namespace->stmts as $stmt) {
|
||||||
|
if ($stmt instanceof Node\Stmt\Use_ || $stmt instanceof Node\Stmt\GroupUse) {
|
||||||
|
foreach ($stmt->uses as $use) {
|
||||||
|
// Get the definition for the used namespace, class-like, function or constant
|
||||||
|
// And save it under the alias
|
||||||
|
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
|
||||||
|
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use statements are always the first statements in a namespace
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If there is a prefix that does not start with a slash, suggest `use`d symbols
|
||||||
|
if ($prefix && !$isFullyQualified) {
|
||||||
|
// Suggest symbols that have been `use`d
|
||||||
|
// Search the aliases for the typed-in name
|
||||||
|
foreach ($aliasedDefs as $alias => $def) {
|
||||||
|
if (substr($alias, 0, $prefixLen) === $prefix) {
|
||||||
|
$list->items[] = CompletionItem::fromDefinition($def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Additionally, suggest global symbols that either
|
||||||
|
// - start with the current namespace + prefix, if the Name node is not fully qualified
|
||||||
|
// - start with just the prefix, if the Name node is fully qualified
|
||||||
|
foreach ($this->project->getDefinitions() as $fqn => $def) {
|
||||||
|
if (
|
||||||
|
$def->isGlobal // exclude methods, properties etc.
|
||||||
|
&& (
|
||||||
|
!$prefix
|
||||||
|
|| (
|
||||||
|
((!$namespace || $isFullyQualified) && substr($fqn, 0, $prefixLen) === $prefix)
|
||||||
|
|| (
|
||||||
|
$namespace
|
||||||
|
&& !$isFullyQualified
|
||||||
|
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Only suggest classes for `new`
|
||||||
|
&& (!($node instanceof Node\Expr\New_) || $def->canBeInstantiated)
|
||||||
|
) {
|
||||||
|
$item = CompletionItem::fromDefinition($def);
|
||||||
|
// Find the shortest name to reference the symbol
|
||||||
|
if ($namespace && ($alias = array_search($def, $aliasedDefs, true)) !== false) {
|
||||||
|
// $alias is the name under which this definition is aliased in the current namespace
|
||||||
|
$item->insertText = $alias;
|
||||||
|
} else if ($namespace && !($prefix && $isFullyQualified)) {
|
||||||
|
// Insert the global FQN with trailing backslash
|
||||||
|
$item->insertText = '\\' . $fqn;
|
||||||
|
} else {
|
||||||
|
// Insert the FQN without trailing backlash
|
||||||
|
$item->insertText = $fqn;
|
||||||
|
}
|
||||||
|
$list->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Suggest keywords
|
||||||
|
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
|
||||||
|
foreach (self::KEYWORDS as $keyword) {
|
||||||
|
if (substr($keyword, 0, $prefixLen) === $prefix) {
|
||||||
|
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
|
||||||
|
$item->insertText = $keyword . ' ';
|
||||||
|
$list->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
$node instanceof Node\Expr\Variable
|
||||||
|
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
|
||||||
|
) {
|
||||||
|
// Find variables, parameters and use statements in the scope
|
||||||
|
// If there was only a $ typed, $node will be instanceof Node\Error
|
||||||
|
$namePrefix = $node instanceof Node\Expr\Variable && is_string($node->name) ? $node->name : '';
|
||||||
|
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
|
||||||
|
$item = new CompletionItem;
|
||||||
|
$item->kind = CompletionItemKind::VARIABLE;
|
||||||
|
$item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name);
|
||||||
|
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
|
||||||
|
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
|
||||||
|
$item->textEdit = new TextEdit(
|
||||||
|
new Range($pos, $pos),
|
||||||
|
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
|
||||||
|
);
|
||||||
|
$list->items[] = $item;
|
||||||
|
}
|
||||||
|
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
|
||||||
|
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
|
||||||
|
$item->textEdit = new TextEdit(
|
||||||
|
new Range($pos, $pos),
|
||||||
|
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
|
||||||
|
);
|
||||||
|
$list->items[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will walk the AST upwards until a function-like node is met
|
||||||
|
* and at each level walk all previous siblings and their children to search for definitions
|
||||||
|
* of that variable
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @param string $namePrefix Prefix to filter
|
||||||
|
* @return array <Node\Expr\Variable|Node\Param|Node\Expr\ClosureUse>
|
||||||
|
*/
|
||||||
|
private function suggestVariablesAtNode(Node $node, string $namePrefix = ''): array
|
||||||
|
{
|
||||||
|
$vars = [];
|
||||||
|
|
||||||
|
// Find variables in the node itself
|
||||||
|
// When getting completion in the middle of a function, $node will be the function node
|
||||||
|
// so we need to search it
|
||||||
|
foreach ($this->findVariableDefinitionsInNode($node, $namePrefix) as $var) {
|
||||||
|
// Only use the first definition
|
||||||
|
if (!isset($vars[$var->name])) {
|
||||||
|
$vars[$var->name] = $var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the AST upwards until a scope boundary is met
|
||||||
|
$level = $node;
|
||||||
|
while ($level && !($level instanceof Node\FunctionLike)) {
|
||||||
|
// Walk siblings before the node
|
||||||
|
$sibling = $level;
|
||||||
|
while ($sibling = $sibling->getAttribute('previousSibling')) {
|
||||||
|
// Collect all variables inside the sibling node
|
||||||
|
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
|
||||||
|
$vars[$var->name] = $var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$level = $level->getAttribute('parentNode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the traversal ended because a function was met,
|
||||||
|
// also add its parameters and closure uses to the result list
|
||||||
|
if ($level instanceof Node\FunctionLike) {
|
||||||
|
foreach ($level->params as $param) {
|
||||||
|
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) {
|
||||||
|
$vars[$param->name] = $param;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($level instanceof Node\Expr\Closure) {
|
||||||
|
foreach ($level->uses as $use) {
|
||||||
|
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) {
|
||||||
|
$vars[$use->var] = $use;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches the subnodes of a node for variable assignments
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @param string $namePrefix Prefix to filter
|
||||||
|
* @return Node\Expr\Variable[]
|
||||||
|
*/
|
||||||
|
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
|
||||||
|
{
|
||||||
|
$vars = [];
|
||||||
|
// If the child node is a variable assignment, save it
|
||||||
|
$parent = $node->getAttribute('parentNode');
|
||||||
|
if (
|
||||||
|
$node instanceof Node\Expr\Variable
|
||||||
|
&& ($parent instanceof Node\Expr\Assign || $parent instanceof Node\Expr\AssignOp)
|
||||||
|
&& is_string($node->name) // Variable variables are of no use
|
||||||
|
&& substr($node->name, 0, strlen($namePrefix)) === $namePrefix
|
||||||
|
) {
|
||||||
|
$vars[] = $node;
|
||||||
|
}
|
||||||
|
// Iterate over subnodes
|
||||||
|
foreach ($node->getSubNodeNames() as $attr) {
|
||||||
|
if (!isset($node->$attr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$children = is_array($node->$attr) ? $node->$attr : [$node->$attr];
|
||||||
|
foreach ($children as $child) {
|
||||||
|
// Dont try to traverse scalars
|
||||||
|
// Dont traverse functions, the contained variables are in a different scope
|
||||||
|
if (!($child instanceof Node) || $child instanceof Node\FunctionLike) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($this->findVariableDefinitionsInNode($child, $namePrefix) as $var) {
|
||||||
|
$vars[] = $var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $vars;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,17 +18,40 @@ class Definition
|
||||||
*
|
*
|
||||||
* Examples of FQNs:
|
* Examples of FQNs:
|
||||||
* - testFunction()
|
* - testFunction()
|
||||||
|
* - TestNamespace
|
||||||
* - TestNamespace\TestClass
|
* - TestNamespace\TestClass
|
||||||
* - TestNamespace\TestClass::TEST_CONSTANT
|
* - TestNamespace\TestClass::TEST_CONSTANT
|
||||||
* - TestNamespace\TestClass::staticTestProperty
|
* - TestNamespace\TestClass::$staticTestProperty
|
||||||
* - TestNamespace\TestClass::testProperty
|
* - TestNamespace\TestClass->testProperty
|
||||||
* - TestNamespace\TestClass::staticTestMethod()
|
* - TestNamespace\TestClass::staticTestMethod()
|
||||||
* - TestNamespace\TestClass::testMethod()
|
* - TestNamespace\TestClass->testMethod()
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
public $fqn;
|
public $fqn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only true for classes, interfaces, traits, functions and non-class constants
|
||||||
|
* This is so methods and properties are not suggested in the global scope
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $isGlobal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* False for instance methods and properties
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $isStatic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the Definition is a class
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $canBeInstantiated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Protocol\SymbolInformation
|
* @var Protocol\SymbolInformation
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -91,6 +91,35 @@ class DefinitionResolver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Definition for a definition node
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @param string $fqn
|
||||||
|
* @return Definition
|
||||||
|
*/
|
||||||
|
public function createDefinitionFromNode(Node $node, string $fqn = null): Definition
|
||||||
|
{
|
||||||
|
$def = new Definition;
|
||||||
|
$def->canBeInstantiated = $node instanceof Node\Stmt\Class_;
|
||||||
|
$def->isGlobal = (
|
||||||
|
$node instanceof Node\Stmt\ClassLike
|
||||||
|
|| $node instanceof Node\Stmt\Namespace_
|
||||||
|
|| $node instanceof Node\Stmt\Function_
|
||||||
|
|| $node->getAttribute('parentNode') instanceof Node\Stmt\Const_
|
||||||
|
);
|
||||||
|
$def->isStatic = (
|
||||||
|
($node instanceof Node\Stmt\ClassMethod && $node->isStatic())
|
||||||
|
|| ($node instanceof Node\Stmt\PropertyProperty && $node->getAttribute('parentNode')->isStatic())
|
||||||
|
);
|
||||||
|
$def->fqn = $fqn;
|
||||||
|
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
||||||
|
$def->type = $this->getTypeFromNode($node);
|
||||||
|
$def->declarationLine = $this->getDeclarationLineFromNode($node);
|
||||||
|
$def->documentation = $this->getDocumentationFromNode($node);
|
||||||
|
return $def;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given any node, returns the Definition object of the symbol that is referenced
|
* Given any node, returns the Definition object of the symbol that is referenced
|
||||||
*
|
*
|
||||||
|
@ -106,21 +135,7 @@ class DefinitionResolver
|
||||||
if ($defNode === null) {
|
if ($defNode === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$def = new Definition;
|
return $this->createDefinitionFromNode($defNode);
|
||||||
// Get symbol information from node (range, symbol kind)
|
|
||||||
$def->symbolInformation = SymbolInformation::fromNode($defNode);
|
|
||||||
// Declaration line
|
|
||||||
$def->declarationLine = $this->getDeclarationLineFromNode($defNode);
|
|
||||||
// Documentation
|
|
||||||
$def->documentation = $this->getDocumentationFromNode($defNode);
|
|
||||||
if ($defNode instanceof Node\Param) {
|
|
||||||
// Get parameter type
|
|
||||||
$def->type = $this->getTypeFromNode($defNode);
|
|
||||||
} else {
|
|
||||||
// Resolve the type of the assignment/closure use node
|
|
||||||
$def->type = $this->resolveExpressionNodeToType($defNode);
|
|
||||||
}
|
|
||||||
return $def;
|
|
||||||
}
|
}
|
||||||
// Other references are references to a global symbol that have an FQN
|
// Other references are references to a global symbol that have an FQN
|
||||||
// Find out the FQN
|
// Find out the FQN
|
||||||
|
@ -136,6 +151,31 @@ class DefinitionResolver
|
||||||
return $this->project->getDefinition($fqn, $globalFallback);
|
return $this->project->getDefinition($fqn, $globalFallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all possible FQNs in a type
|
||||||
|
*
|
||||||
|
* @param Type $type
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function getFqnsFromType(Type $type): array
|
||||||
|
{
|
||||||
|
$fqns = [];
|
||||||
|
if ($type instanceof Types\Object_) {
|
||||||
|
$fqsen = $type->getFqsen();
|
||||||
|
if ($fqsen !== null) {
|
||||||
|
$fqns[] = substr((string)$fqsen, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($type instanceof Types\Compound) {
|
||||||
|
for ($i = 0; $t = $type->get($i); $i++) {
|
||||||
|
foreach (self::getFqnsFromType($type) as $fqn) {
|
||||||
|
$fqns[] = $fqn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fqns;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given any node, returns the FQN of the symbol that is referenced
|
* Given any node, returns the FQN of the symbol that is referenced
|
||||||
* Returns null if the FQN could not be resolved or the reference node references a variable
|
* Returns null if the FQN could not be resolved or the reference node references a variable
|
||||||
|
@ -150,6 +190,7 @@ class DefinitionResolver
|
||||||
if (
|
if (
|
||||||
$node instanceof Node\Name && (
|
$node instanceof Node\Name && (
|
||||||
$parent instanceof Node\Stmt\ClassLike
|
$parent instanceof Node\Stmt\ClassLike
|
||||||
|
|| $parent instanceof Node\Namespace_
|
||||||
|| $parent instanceof Node\Param
|
|| $parent instanceof Node\Param
|
||||||
|| $parent instanceof Node\FunctionLike
|
|| $parent instanceof Node\FunctionLike
|
||||||
|| $parent instanceof Node\Expr\StaticCall
|
|| $parent instanceof Node\Expr\StaticCall
|
||||||
|
@ -211,7 +252,7 @@ class DefinitionResolver
|
||||||
} else {
|
} else {
|
||||||
$classFqn = substr((string)$varType->getFqsen(), 1);
|
$classFqn = substr((string)$varType->getFqsen(), 1);
|
||||||
}
|
}
|
||||||
$name = $classFqn . '::' . (string)$node->name;
|
$name = $classFqn . '->' . (string)$node->name;
|
||||||
} else if ($parent instanceof Node\Expr\FuncCall) {
|
} else if ($parent instanceof Node\Expr\FuncCall) {
|
||||||
if ($parent->name instanceof Node\Expr) {
|
if ($parent->name instanceof Node\Expr) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -245,7 +286,11 @@ class DefinitionResolver
|
||||||
$className = (string)$classNode->namespacedName;
|
$className = (string)$classNode->namespacedName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($node instanceof Node\Expr\StaticPropertyFetch) {
|
||||||
|
$name = (string)$className . '::$' . $node->name;
|
||||||
|
} else {
|
||||||
$name = (string)$className . '::' . $node->name;
|
$name = (string)$className . '::' . $node->name;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -281,25 +326,34 @@ class DefinitionResolver
|
||||||
/**
|
/**
|
||||||
* Returns the assignment or parameter node where a variable was defined
|
* Returns the assignment or parameter node where a variable was defined
|
||||||
*
|
*
|
||||||
* @param Node\Expr\Variable $n The variable access
|
* @param Node\Expr\Variable|Node\Expr\ClosureUse $var The variable access
|
||||||
* @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null
|
* @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null
|
||||||
*/
|
*/
|
||||||
public static function resolveVariableToNode(Node\Expr\Variable $var)
|
public static function resolveVariableToNode(Node\Expr $var)
|
||||||
{
|
{
|
||||||
$n = $var;
|
$n = $var;
|
||||||
|
// When a use is passed, start outside the closure to not return immediatly
|
||||||
|
if ($var instanceof Node\Expr\ClosureUse) {
|
||||||
|
$n = $var->getAttribute('parentNode')->getAttribute('parentNode');
|
||||||
|
$name = $var->var;
|
||||||
|
} else if ($var instanceof Node\Expr\Variable || $var instanceof Node\Param) {
|
||||||
|
$name = $var->name;
|
||||||
|
} else {
|
||||||
|
throw new \InvalidArgumentException('$var must be Variable, Param or ClosureUse, not ' . get_class($var));
|
||||||
|
}
|
||||||
// Traverse the AST up
|
// Traverse the AST up
|
||||||
do {
|
do {
|
||||||
// If a function is met, check the parameters and use statements
|
// If a function is met, check the parameters and use statements
|
||||||
if ($n instanceof Node\FunctionLike) {
|
if ($n instanceof Node\FunctionLike) {
|
||||||
foreach ($n->getParams() as $param) {
|
foreach ($n->getParams() as $param) {
|
||||||
if ($param->name === $var->name) {
|
if ($param->name === $name) {
|
||||||
return $param;
|
return $param;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If it is a closure, also check use statements
|
// If it is a closure, also check use statements
|
||||||
if ($n instanceof Node\Expr\Closure) {
|
if ($n instanceof Node\Expr\Closure) {
|
||||||
foreach ($n->uses as $use) {
|
foreach ($n->uses as $use) {
|
||||||
if ($use->var === $var->name) {
|
if ($use->var === $name) {
|
||||||
return $use;
|
return $use;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -310,7 +364,7 @@ class DefinitionResolver
|
||||||
while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) {
|
while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) {
|
||||||
if (
|
if (
|
||||||
($n instanceof Node\Expr\Assign || $n instanceof Node\Expr\AssignOp)
|
($n instanceof Node\Expr\Assign || $n instanceof Node\Expr\AssignOp)
|
||||||
&& $n->var instanceof Node\Expr\Variable && $n->var->name === $var->name
|
&& $n->var instanceof Node\Expr\Variable && $n->var->name === $name
|
||||||
) {
|
) {
|
||||||
return $n;
|
return $n;
|
||||||
}
|
}
|
||||||
|
@ -327,10 +381,10 @@ class DefinitionResolver
|
||||||
* @param \PhpParser\Node\Expr $expr
|
* @param \PhpParser\Node\Expr $expr
|
||||||
* @return \phpDocumentor\Type
|
* @return \phpDocumentor\Type
|
||||||
*/
|
*/
|
||||||
private function resolveExpressionNodeToType(Node\Expr $expr): Type
|
public function resolveExpressionNodeToType(Node\Expr $expr): Type
|
||||||
{
|
{
|
||||||
if ($expr instanceof Node\Expr\Variable) {
|
if ($expr instanceof Node\Expr\Variable || $expr instanceof Node\Expr\ClosureUse) {
|
||||||
if ($expr->name === 'this') {
|
if ($expr instanceof Node\Expr\Variable && $expr->name === 'this') {
|
||||||
return new Types\This;
|
return new Types\This;
|
||||||
}
|
}
|
||||||
// Find variable definition
|
// Find variable definition
|
||||||
|
@ -385,7 +439,7 @@ class DefinitionResolver
|
||||||
} else {
|
} else {
|
||||||
$classFqn = substr((string)$t->getFqsen(), 1);
|
$classFqn = substr((string)$t->getFqsen(), 1);
|
||||||
}
|
}
|
||||||
$fqn = $classFqn . '::' . $expr->name;
|
$fqn = $classFqn . '->' . $expr->name;
|
||||||
if ($expr instanceof Node\Expr\MethodCall) {
|
if ($expr instanceof Node\Expr\MethodCall) {
|
||||||
$fqn .= '()';
|
$fqn .= '()';
|
||||||
}
|
}
|
||||||
|
@ -404,7 +458,11 @@ class DefinitionResolver
|
||||||
if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) {
|
if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) {
|
||||||
return new Types\Mixed;
|
return new Types\Mixed;
|
||||||
}
|
}
|
||||||
$fqn = substr((string)$classType->getFqsen(), 1) . '::' . $expr->name;
|
$fqn = substr((string)$classType->getFqsen(), 1) . '::';
|
||||||
|
if ($expr instanceof Node\Expr\StaticPropertyFetch) {
|
||||||
|
$fqn .= '$';
|
||||||
|
}
|
||||||
|
$fqn .= $expr->name;
|
||||||
if ($expr instanceof Node\Expr\StaticCall) {
|
if ($expr instanceof Node\Expr\StaticCall) {
|
||||||
$fqn .= '()';
|
$fqn .= '()';
|
||||||
}
|
}
|
||||||
|
@ -599,7 +657,7 @@ class DefinitionResolver
|
||||||
* For functions and methods, this is the return type.
|
* For functions and methods, this is the return type.
|
||||||
* For parameters, this is the type of the parameter.
|
* For parameters, this is the type of the parameter.
|
||||||
* For classes and interfaces, this is the class type (object).
|
* For classes and interfaces, this is the class type (object).
|
||||||
* Variables are not indexed for performance reasons.
|
* For variables / assignments, this is the documented type or type the assignment resolves to.
|
||||||
* Can also be a compound type.
|
* Can also be a compound type.
|
||||||
* If it is unknown, will be Types\Mixed.
|
* If it is unknown, will be Types\Mixed.
|
||||||
* Returns null if the node does not have a type.
|
* Returns null if the node does not have a type.
|
||||||
|
@ -612,28 +670,35 @@ class DefinitionResolver
|
||||||
if ($node instanceof Node\Param) {
|
if ($node instanceof Node\Param) {
|
||||||
// Parameters
|
// Parameters
|
||||||
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
||||||
if (
|
if ($docBlock !== null) {
|
||||||
$docBlock !== null
|
|
||||||
&& !empty($paramTags = $docBlock->getTagsByName('param'))
|
|
||||||
&& $paramTags[0]->getType() !== null
|
|
||||||
) {
|
|
||||||
// Use @param tag
|
// Use @param tag
|
||||||
return $paramTags[0]->getType();
|
foreach ($docBlock->getTagsByName('param') as $paramTag) {
|
||||||
|
if ($paramTag->getVariableName() === $node->name) {
|
||||||
|
if ($paramTag->getType() === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $paramTag->getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($node->type !== null) {
|
if ($node->type !== null) {
|
||||||
// Use PHP7 return type hint
|
// Use PHP7 return type hint
|
||||||
if (is_string($node->type)) {
|
if (is_string($node->type)) {
|
||||||
// Resolve a string like "bool" to a type object
|
// Resolve a string like "bool" to a type object
|
||||||
$type = $this->typeResolver->resolve($node->type);
|
$type = $this->typeResolver->resolve($node->type);
|
||||||
}
|
} else {
|
||||||
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
|
$type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($node->default !== null) {
|
if ($node->default !== null) {
|
||||||
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
$defaultType = $this->resolveExpressionNodeToType($node->default);
|
||||||
|
if (isset($type) && !is_a($type, get_class($defaultType))) {
|
||||||
$type = new Types\Compound([$type, $defaultType]);
|
$type = new Types\Compound([$type, $defaultType]);
|
||||||
|
} else {
|
||||||
|
$type = $defaultType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unknown parameter type
|
return $type ?? new Types\Mixed;
|
||||||
return new Types\Mixed;
|
|
||||||
}
|
}
|
||||||
if ($node instanceof Node\FunctionLike) {
|
if ($node instanceof Node\FunctionLike) {
|
||||||
// Functions/methods
|
// Functions/methods
|
||||||
|
@ -657,16 +722,39 @@ class DefinitionResolver
|
||||||
// Unknown return type
|
// Unknown return type
|
||||||
return new Types\Mixed;
|
return new Types\Mixed;
|
||||||
}
|
}
|
||||||
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
|
if ($node instanceof Node\Expr\Variable) {
|
||||||
// Property or constant
|
$node = $node->getAttribute('parentNode');
|
||||||
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
|
}
|
||||||
if (
|
if (
|
||||||
$docBlock !== null
|
$node instanceof Node\Stmt\PropertyProperty
|
||||||
&& !empty($varTags = $docBlock->getTagsByName('var'))
|
|| $node instanceof Node\Const_
|
||||||
&& $varTags[0]->getType()
|
|| $node instanceof Node\Expr\Assign
|
||||||
|
|| $node instanceof Node\Expr\AssignOp
|
||||||
) {
|
) {
|
||||||
|
if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) {
|
||||||
|
$docBlockHolder = $node->getAttribute('parentNode');
|
||||||
|
} else {
|
||||||
|
$docBlockHolder = $node;
|
||||||
|
}
|
||||||
|
// Property, constant or variable
|
||||||
// Use @var tag
|
// Use @var tag
|
||||||
return $varTags[0]->getType();
|
if (
|
||||||
|
isset($docBlockHolder)
|
||||||
|
&& ($docBlock = $docBlockHolder->getAttribute('docBlock'))
|
||||||
|
&& !empty($varTags = $docBlock->getTagsByName('var'))
|
||||||
|
&& ($type = $varTags[0]->getType())
|
||||||
|
) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
// Resolve the expression
|
||||||
|
if ($node instanceof Node\Stmt\PropertyProperty) {
|
||||||
|
if ($node->default) {
|
||||||
|
return $this->resolveExpressionNodeToType($node->default);
|
||||||
|
}
|
||||||
|
} else if ($node instanceof Node\Const_) {
|
||||||
|
return $this->resolveExpressionNodeToType($node->value);
|
||||||
|
} else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
|
||||||
|
return $this->resolveExpressionNodeToType($node);
|
||||||
}
|
}
|
||||||
// TODO: read @property tags of class
|
// TODO: read @property tags of class
|
||||||
// TODO: Try to infer the type from default value / constant value
|
// TODO: Try to infer the type from default value / constant value
|
||||||
|
@ -689,25 +777,37 @@ class DefinitionResolver
|
||||||
if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) {
|
if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) {
|
||||||
// Class, interface or trait declaration
|
// Class, interface or trait declaration
|
||||||
return (string)$node->namespacedName;
|
return (string)$node->namespacedName;
|
||||||
|
} else if ($node instanceof Node\Stmt\Namespace_) {
|
||||||
|
return (string)$node->name;
|
||||||
} else if ($node instanceof Node\Stmt\Function_) {
|
} else if ($node instanceof Node\Stmt\Function_) {
|
||||||
// Function: use functionName() as the name
|
// Function: use functionName() as the name
|
||||||
return (string)$node->namespacedName . '()';
|
return (string)$node->namespacedName . '()';
|
||||||
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
||||||
// Class method: use ClassName::methodName() as name
|
// Class method: use ClassName->methodName() as name
|
||||||
$class = $node->getAttribute('parentNode');
|
$class = $node->getAttribute('parentNode');
|
||||||
if (!isset($class->name)) {
|
if (!isset($class->name)) {
|
||||||
// Ignore anonymous classes
|
// Ignore anonymous classes
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if ($node->isStatic()) {
|
||||||
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
|
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
|
||||||
|
} else {
|
||||||
|
return (string)$class->namespacedName . '->' . (string)$node->name . '()';
|
||||||
|
}
|
||||||
} else if ($node instanceof Node\Stmt\PropertyProperty) {
|
} else if ($node instanceof Node\Stmt\PropertyProperty) {
|
||||||
// Property: use ClassName::propertyName as name
|
$property = $node->getAttribute('parentNode');
|
||||||
$class = $node->getAttribute('parentNode')->getAttribute('parentNode');
|
$class = $property->getAttribute('parentNode');
|
||||||
if (!isset($class->name)) {
|
if (!isset($class->name)) {
|
||||||
// Ignore anonymous classes
|
// Ignore anonymous classes
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (string)$class->namespacedName . '::' . (string)$node->name;
|
if ($property->isStatic()) {
|
||||||
|
// Static Property: use ClassName::$propertyName as name
|
||||||
|
return (string)$class->namespacedName . '::$' . (string)$node->name;
|
||||||
|
} else {
|
||||||
|
// Instance Property: use ClassName->propertyName as name
|
||||||
|
return (string)$class->namespacedName . '->' . (string)$node->name;
|
||||||
|
}
|
||||||
} else if ($node instanceof Node\Const_) {
|
} else if ($node instanceof Node\Const_) {
|
||||||
$parent = $node->getAttribute('parentNode');
|
$parent = $node->getAttribute('parentNode');
|
||||||
if ($parent instanceof Node\Stmt\Const_) {
|
if ($parent instanceof Node\Stmt\Const_) {
|
||||||
|
|
|
@ -11,7 +11,8 @@ use LanguageServer\Protocol\{
|
||||||
MessageType,
|
MessageType,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
SymbolInformation,
|
SymbolInformation,
|
||||||
TextDocumentIdentifier
|
TextDocumentIdentifier,
|
||||||
|
CompletionOptions
|
||||||
};
|
};
|
||||||
use AdvancedJsonRpc;
|
use AdvancedJsonRpc;
|
||||||
use Sabre\Event\{Loop, Promise};
|
use Sabre\Event\{Loop, Promise};
|
||||||
|
@ -144,6 +145,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->referencesProvider = true;
|
$serverCapabilities->referencesProvider = true;
|
||||||
// Support "Hover"
|
// Support "Hover"
|
||||||
$serverCapabilities->hoverProvider = true;
|
$serverCapabilities->hoverProvider = true;
|
||||||
|
// Support "Completion"
|
||||||
|
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||||
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||||
|
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,13 +42,6 @@ class DefinitionCollector extends NodeVisitorAbstract
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->nodes[$fqn] = $node;
|
$this->nodes[$fqn] = $node;
|
||||||
$def = new Definition;
|
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
|
||||||
$def->fqn = $fqn;
|
|
||||||
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
|
||||||
$def->type = $this->definitionResolver->getTypeFromNode($node);
|
|
||||||
$def->declarationLine = $this->definitionResolver->getDeclarationLineFromNode($node);
|
|
||||||
$def->documentation = $this->definitionResolver->getDocumentationFromNode($node);
|
|
||||||
|
|
||||||
$this->definitions[$fqn] = $def;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -290,6 +290,9 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
public function getNodeAtPosition(Position $position)
|
public function getNodeAtPosition(Position $position)
|
||||||
{
|
{
|
||||||
|
if ($this->stmts === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
$finder = new NodeAtPositionFinder($position);
|
$finder = new NodeAtPositionFinder($position);
|
||||||
$traverser->addVisitor($finder);
|
$traverser->addVisitor($finder);
|
||||||
|
@ -297,6 +300,22 @@ class PhpDocument
|
||||||
return $finder->node;
|
return $finder->node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a range of the content
|
||||||
|
*
|
||||||
|
* @param Range $range
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getRange(Range $range)
|
||||||
|
{
|
||||||
|
if ($this->content === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$start = $range->start->toOffset($this->content);
|
||||||
|
$length = $range->end->toOffset($this->content) - $start;
|
||||||
|
return substr($this->content, $start, $length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the definition node for a fully qualified name
|
* Returns the definition node for a fully qualified name
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use LanguageServer\Definition;
|
||||||
|
|
||||||
class CompletionItem
|
class CompletionItem
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -69,6 +72,24 @@ class CompletionItem
|
||||||
*/
|
*/
|
||||||
public $textEdit;
|
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
|
* An data entry field that is preserved on a completion item between
|
||||||
* a completion and a completion resolve request.
|
* a completion and a completion resolve request.
|
||||||
|
@ -76,4 +97,68 @@ class CompletionItem
|
||||||
* @var mixed
|
* @var mixed
|
||||||
*/
|
*/
|
||||||
public $data;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ abstract class CompletionItemKind
|
||||||
const CONSTRUCTOR = 4;
|
const CONSTRUCTOR = 4;
|
||||||
const FIELD = 5;
|
const FIELD = 5;
|
||||||
const VARIABLE = 6;
|
const VARIABLE = 6;
|
||||||
const _CLASS = 7;
|
const CLASS_ = 7;
|
||||||
const INTERFACE = 8;
|
const INTERFACE = 8;
|
||||||
const MODULE = 9;
|
const MODULE = 9;
|
||||||
const PROPERTY = 10;
|
const PROPERTY = 10;
|
||||||
|
@ -25,4 +25,46 @@ abstract class CompletionItemKind
|
||||||
const COLOR = 16;
|
const COLOR = 16;
|
||||||
const FILE = 17;
|
const FILE = 17;
|
||||||
const REFERENCE = 18;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,4 +22,14 @@ class CompletionList
|
||||||
* @var CompletionItem[]
|
* @var CompletionItem[]
|
||||||
*/
|
*/
|
||||||
public $items;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,14 +11,14 @@ class CompletionOptions
|
||||||
* The server provides support to resolve additional information for a completion
|
* The server provides support to resolve additional information for a completion
|
||||||
* item.
|
* item.
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool|null
|
||||||
*/
|
*/
|
||||||
public $resolveProvider;
|
public $resolveProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The characters that trigger completion automatically.
|
* The characters that trigger completion automatically.
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string[]|null
|
||||||
*/
|
*/
|
||||||
public $triggerCharacters;
|
public $triggerCharacters;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,4 +49,17 @@ class Position
|
||||||
|
|
||||||
return $this->character - $position->character;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ class SymbolInformation
|
||||||
}
|
}
|
||||||
$symbol->location = Location::fromNode($node);
|
$symbol->location = Location::fromNode($node);
|
||||||
if ($fqn !== null) {
|
if ($fqn !== null) {
|
||||||
$parts = preg_split('/(::|\\\\)/', $fqn);
|
$parts = preg_split('/(::|->|\\\\)/', $fqn);
|
||||||
array_pop($parts);
|
array_pop($parts);
|
||||||
$symbol->containerName = implode('\\', $parts);
|
$symbol->containerName = implode('\\', $parts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver};
|
use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider};
|
||||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
use PhpParser\Node;
|
use PhpParser\Node;
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
|
@ -18,7 +18,10 @@ use LanguageServer\Protocol\{
|
||||||
SymbolInformation,
|
SymbolInformation,
|
||||||
ReferenceContext,
|
ReferenceContext,
|
||||||
Hover,
|
Hover,
|
||||||
MarkedString
|
MarkedString,
|
||||||
|
SymbolKind,
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind
|
||||||
};
|
};
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use function Sabre\Event\coroutine;
|
use function Sabre\Event\coroutine;
|
||||||
|
@ -50,12 +53,18 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
private $definitionResolver;
|
private $definitionResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CompletionProvider
|
||||||
|
*/
|
||||||
|
private $completionProvider;
|
||||||
|
|
||||||
public function __construct(Project $project, LanguageClient $client)
|
public function __construct(Project $project, LanguageClient $client)
|
||||||
{
|
{
|
||||||
$this->project = $project;
|
$this->project = $project;
|
||||||
$this->client = $client;
|
$this->client = $client;
|
||||||
$this->prettyPrinter = new PrettyPrinter();
|
$this->prettyPrinter = new PrettyPrinter();
|
||||||
$this->definitionResolver = new DefinitionResolver($project);
|
$this->definitionResolver = new DefinitionResolver($project);
|
||||||
|
$this->completionProvider = new CompletionProvider($this->definitionResolver, $project);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,4 +219,26 @@ class TextDocument
|
||||||
return new Hover($contents, $range);
|
return new Hover($contents, $range);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Completion request is sent from the client to the server to compute completion items at a given cursor
|
||||||
|
* position. Completion items are presented in the IntelliSense user interface. If computing full completion items
|
||||||
|
* is expensive, servers can additionally provide a handler for the completion item resolve request
|
||||||
|
* ('completionItem/resolve'). This request is sent when a completion item is selected in the user interface. A
|
||||||
|
* typically use case is for example: the 'textDocument/completion' request doesn't fill in the documentation
|
||||||
|
* property for returned completion items since it is expensive to compute. When the item is selected in the user
|
||||||
|
* interface then a 'completionItem/resolve' request is sent with the selected completion item as a param. The
|
||||||
|
* returned completion item should have the documentation property filled in.
|
||||||
|
*
|
||||||
|
* @param TextDocumentIdentifier The text document
|
||||||
|
* @param Position $position The position
|
||||||
|
* @return Promise <CompletionItem[]|CompletionList>
|
||||||
|
*/
|
||||||
|
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
|
{
|
||||||
|
return coroutine(function () use ($textDocument, $position) {
|
||||||
|
$document = yield $this->project->getOrLoadDocument($textDocument->uri);
|
||||||
|
return $this->completionProvider->provideCompletion($document, $position);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,3 +95,25 @@ function getClosestNode(Node $node, string $type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the part of $b that is not overlapped by $a
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* stripStringOverlap('whatever<?', '<?php') === 'php'
|
||||||
|
*
|
||||||
|
* @param string $a
|
||||||
|
* @param string $b
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function stripStringOverlap(string $a, string $b): string
|
||||||
|
{
|
||||||
|
$aLen = strlen($a);
|
||||||
|
$bLen = strlen($b);
|
||||||
|
for ($i = 1; $i <= $bLen; $i++) {
|
||||||
|
if (substr($b, 0, $i) === substr($a, $aLen - $i)) {
|
||||||
|
return substr($b, $i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $b;
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,10 @@ class LanguageServerTest extends TestCase
|
||||||
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
||||||
'documentSymbolProvider' => true,
|
'documentSymbolProvider' => true,
|
||||||
'hoverProvider' => true,
|
'hoverProvider' => true,
|
||||||
'completionProvider' => null,
|
'completionProvider' => (object)[
|
||||||
|
'resolveProvider' => false,
|
||||||
|
'triggerCharacters' => ['$', '>']
|
||||||
|
],
|
||||||
'signatureHelpProvider' => null,
|
'signatureHelpProvider' => null,
|
||||||
'definitionProvider' => true,
|
'definitionProvider' => true,
|
||||||
'referencesProvider' => true,
|
'referencesProvider' => true,
|
||||||
|
@ -61,7 +64,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||||
if ($msg->body->params->type === MessageType::ERROR) {
|
if ($msg->body->params->type === MessageType::ERROR) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
} else if (strpos($msg->body->params->message, 'All 10 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) {
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +109,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($promise->state === Promise::PENDING) {
|
if ($promise->state === Promise::PENDING) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
}
|
}
|
||||||
} else if (strpos($msg->body->params->message, 'All 10 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) {
|
||||||
// Indexing finished
|
// Indexing finished
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,14 @@ class DefinitionCollectorTest extends TestCase
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
$defNodes = $definitionCollector->nodes;
|
$defNodes = $definitionCollector->nodes;
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
|
'TestNamespace',
|
||||||
'TestNamespace\\TEST_CONST',
|
'TestNamespace\\TEST_CONST',
|
||||||
'TestNamespace\\TestClass',
|
'TestNamespace\\TestClass',
|
||||||
'TestNamespace\\TestClass::TEST_CLASS_CONST',
|
'TestNamespace\\TestClass::TEST_CLASS_CONST',
|
||||||
'TestNamespace\\TestClass::staticTestProperty',
|
'TestNamespace\\TestClass::$staticTestProperty',
|
||||||
'TestNamespace\\TestClass::testProperty',
|
'TestNamespace\\TestClass->testProperty',
|
||||||
'TestNamespace\\TestClass::staticTestMethod()',
|
'TestNamespace\\TestClass::staticTestMethod()',
|
||||||
'TestNamespace\\TestClass::testMethod()',
|
'TestNamespace\\TestClass->testMethod()',
|
||||||
'TestNamespace\\TestTrait',
|
'TestNamespace\\TestTrait',
|
||||||
'TestNamespace\\TestInterface',
|
'TestNamespace\\TestInterface',
|
||||||
'TestNamespace\\test_function()'
|
'TestNamespace\\test_function()'
|
||||||
|
@ -44,10 +45,10 @@ class DefinitionCollectorTest extends TestCase
|
||||||
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
|
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']);
|
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']);
|
||||||
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
||||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::staticTestProperty']);
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
|
||||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::testProperty']);
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass->testProperty']);
|
||||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
|
||||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::testMethod()']);
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']);
|
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']);
|
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']);
|
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']);
|
||||||
|
@ -68,7 +69,8 @@ class DefinitionCollectorTest extends TestCase
|
||||||
$stmts = $parser->parse(file_get_contents($uri));
|
$stmts = $parser->parse(file_get_contents($uri));
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
$defNodes = $definitionCollector->nodes;
|
$defNodes = $definitionCollector->nodes;
|
||||||
$this->assertEquals(['TestNamespace\\whatever()'], array_keys($defNodes));
|
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
|
||||||
|
$this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']);
|
||||||
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);
|
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,13 +54,11 @@ abstract class ServerTestCase extends TestCase
|
||||||
$referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
|
$referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
|
||||||
$useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php'));
|
$useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php'));
|
||||||
|
|
||||||
Promise\all([
|
$this->project->loadDocument($symbolsUri)->wait();
|
||||||
$this->project->loadDocument($symbolsUri),
|
$this->project->loadDocument($referencesUri)->wait();
|
||||||
$this->project->loadDocument($referencesUri),
|
$this->project->loadDocument($globalSymbolsUri)->wait();
|
||||||
$this->project->loadDocument($globalSymbolsUri),
|
$this->project->loadDocument($globalReferencesUri)->wait();
|
||||||
$this->project->loadDocument($globalReferencesUri),
|
$this->project->loadDocument($useUri)->wait();
|
||||||
$this->project->loadDocument($useUri)
|
|
||||||
])->wait();
|
|
||||||
|
|
||||||
// @codingStandardsIgnoreStart
|
// @codingStandardsIgnoreStart
|
||||||
$this->definitionLocations = [
|
$this->definitionLocations = [
|
||||||
|
@ -79,6 +77,8 @@ abstract class ServerTestCase extends TestCase
|
||||||
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))),
|
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))),
|
||||||
|
|
||||||
// Namespaced
|
// Namespaced
|
||||||
|
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 0), new Position( 2, 24))),
|
||||||
|
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 0), new Position( 2, 30))),
|
||||||
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
|
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
|
||||||
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
|
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
|
||||||
'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
|
'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
|
||||||
|
|
|
@ -0,0 +1,383 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, LanguageClient, Project, CompletionProvider};
|
||||||
|
use LanguageServer\Protocol\{
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
TextEdit,
|
||||||
|
Range,
|
||||||
|
Position,
|
||||||
|
ClientCapabilities,
|
||||||
|
CompletionList,
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind
|
||||||
|
};
|
||||||
|
use function LanguageServer\pathToUri;
|
||||||
|
|
||||||
|
class CompletionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Project
|
||||||
|
*/
|
||||||
|
private $project;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||||
|
$this->project = new Project($client, new ClientCapabilities);
|
||||||
|
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait();
|
||||||
|
$this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait();
|
||||||
|
$this->textDocument = new Server\TextDocument($this->project, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPropertyAndMethodWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(3, 7)
|
||||||
|
)->wait();
|
||||||
|
$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.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPropertyAndMethodWithoutPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(3, 6)
|
||||||
|
)->wait();
|
||||||
|
$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.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariable()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(8, 5)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'$var',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'int',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(8, 5), new Position(8, 5)), 'var')
|
||||||
|
),
|
||||||
|
new CompletionItem(
|
||||||
|
'$param',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'string|null',
|
||||||
|
'A parameter',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(8, 5), new Position(8, 5)), 'param')
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVariableWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(8, 6)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'$param',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'string|null',
|
||||||
|
'A parameter',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(8, 6), new Position(8, 6)), 'aram')
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNewInNamespace()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(6, 10)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
// Global TestClass definition (inserted as \TestClass)
|
||||||
|
new CompletionItem(
|
||||||
|
'TestClass',
|
||||||
|
CompletionItemKind::CLASS_,
|
||||||
|
null,
|
||||||
|
'Pariatur ut laborum tempor voluptate consequat ea deserunt.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'\TestClass'
|
||||||
|
),
|
||||||
|
// Namespaced, `use`d TestClass definition (inserted as TestClass)
|
||||||
|
new CompletionItem(
|
||||||
|
'TestClass',
|
||||||
|
CompletionItemKind::CLASS_,
|
||||||
|
'TestNamespace',
|
||||||
|
'Pariatur ut laborum tempor voluptate consequat ea deserunt.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'TestClass'
|
||||||
|
),
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsedClass()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(6, 5)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'TestClass',
|
||||||
|
CompletionItemKind::CLASS_,
|
||||||
|
'TestNamespace',
|
||||||
|
'Pariatur ut laborum tempor voluptate consequat ea deserunt.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaticPropertyWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 14)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'staticTestProperty',
|
||||||
|
CompletionItemKind::PROPERTY,
|
||||||
|
'\TestClass[]',
|
||||||
|
'Lorem excepteur officia sit anim velit veniam enim.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'$staticTestProperty'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaticWithoutPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 11)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(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(
|
||||||
|
'staticTestMethod',
|
||||||
|
CompletionItemKind::METHOD,
|
||||||
|
'mixed', // Method return type
|
||||||
|
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStaticMethodWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 13)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'staticTestMethod',
|
||||||
|
CompletionItemKind::METHOD,
|
||||||
|
'mixed', // Method return type
|
||||||
|
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClassConstWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 13)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'TEST_CLASS_CONST',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'int',
|
||||||
|
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFullyQualifiedClass()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(6, 6)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'TestClass',
|
||||||
|
CompletionItemKind::CLASS_,
|
||||||
|
null,
|
||||||
|
'Pariatur ut laborum tempor voluptate consequat ea deserunt.',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'TestClass'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKeywords()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(2, 1)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '),
|
||||||
|
new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ')
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHtmlWithoutPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(0, 0)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'<?php',
|
||||||
|
CompletionItemKind::KEYWORD,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), '<?php')
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHtmlWithPrefix()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(0, 1)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'<?php',
|
||||||
|
CompletionItemKind::KEYWORD,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(0, 1), new Position(0, 1)), '?php')
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNamespace()
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php');
|
||||||
|
$this->project->openDocument($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
new Position(4, 6)
|
||||||
|
)->wait();
|
||||||
|
$this->assertEquals(new CompletionList([
|
||||||
|
new CompletionItem(
|
||||||
|
'SomeNamespace',
|
||||||
|
CompletionItemKind::MODULE,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'SomeNamespace'
|
||||||
|
)
|
||||||
|
], true), $items);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ class DocumentSymbolTest extends ServerTestCase
|
||||||
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait();
|
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait();
|
||||||
// @codingStandardsIgnoreStart
|
// @codingStandardsIgnoreStart
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
|
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace'), ''),
|
||||||
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
|
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
|
||||||
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
|
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
|
||||||
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'),
|
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'),
|
||||||
|
|
|
@ -6,7 +6,17 @@ namespace LanguageServer\Tests\Server\Workspace;
|
||||||
use LanguageServer\Tests\MockProtocolStream;
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
use LanguageServer\Tests\Server\ServerTestCase;
|
use LanguageServer\Tests\Server\ServerTestCase;
|
||||||
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument};
|
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument};
|
||||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolInformation, SymbolKind, DiagnosticSeverity, FormattingOptions};
|
use LanguageServer\Protocol\{
|
||||||
|
TextDocumentItem,
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
SymbolInformation,
|
||||||
|
SymbolKind,
|
||||||
|
DiagnosticSeverity,
|
||||||
|
FormattingOptions,
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
Position
|
||||||
|
};
|
||||||
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
||||||
use function LanguageServer\pathToUri;
|
use function LanguageServer\pathToUri;
|
||||||
|
|
||||||
|
@ -16,8 +26,10 @@ class SymbolTest extends ServerTestCase
|
||||||
{
|
{
|
||||||
// Request symbols
|
// Request symbols
|
||||||
$result = $this->workspace->symbol('');
|
$result = $this->workspace->symbol('');
|
||||||
|
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
|
||||||
// @codingStandardsIgnoreStart
|
// @codingStandardsIgnoreStart
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
|
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''),
|
||||||
// Namespaced
|
// Namespaced
|
||||||
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
|
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
|
||||||
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
|
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
|
||||||
|
@ -41,7 +53,9 @@ class SymbolTest extends ServerTestCase
|
||||||
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''),
|
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''),
|
||||||
new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestInterface'), ''),
|
new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestInterface'), ''),
|
||||||
new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('test_function()'), ''),
|
new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('test_function()'), ''),
|
||||||
new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('whatever()'), '')
|
new SymbolInformation('whatever', SymbolKind::FUNCTION, $this->getDefinitionLocation('whatever()'), ''),
|
||||||
|
|
||||||
|
new SymbolInformation('SecondTestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('SecondTestNamespace'), '')
|
||||||
], $result);
|
], $result);
|
||||||
// @codingStandardsIgnoreEnd
|
// @codingStandardsIgnoreEnd
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Utils;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use function LanguageServer\stripStringOverlap;
|
||||||
|
|
||||||
|
class StripStringOverlapTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testNoCharOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('<?php', stripStringOverlap('bla', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test1CharOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('?php', stripStringOverlap('bla<', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test2CharsOverlap()
|
||||||
|
{
|
||||||
|
$this->assertEquals('php', stripStringOverlap('bla<?', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEverythingOverlaps()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('bla<?php', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyA()
|
||||||
|
{
|
||||||
|
$this->assertEquals('<?php', stripStringOverlap('', '<?php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyB()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('bla', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBothEmpty()
|
||||||
|
{
|
||||||
|
$this->assertEquals('', stripStringOverlap('', ''));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue