Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"files" : [
"src/utils.php",
"src/FqnUtilities.php",
"src/ParserHelpers.php"
"src/ParserHelpers.php",
"src/Scope/GetScopeAtNode.php"
]
},
"autoload-dev": {
Expand Down
125 changes: 13 additions & 112 deletions src/CompletionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CompletionContext,
CompletionTriggerKind
};
use function LanguageServer\Scope\getScopeAtNode;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Generator;
Expand Down Expand Up @@ -193,15 +194,22 @@ public function provideCompletion(PhpDocument $doc, Position $pos, CompletionCon
//
// $|
// $a|
//
// TODO: Superglobals

// Find variables, parameters and use statements in the scope
$namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$prefixLen = strlen($namePrefix);
$scope = getScopeAtNode($this->definitionResolver, $node);
$variables = $scope->variables;
foreach ($variables as $name => $var) {
if (substr($name, 0, $prefixLen) !== $namePrefix) {
continue;
}
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->label = '$' . $name;
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var->definitionNode);
$item->detail = (string)$var->type;
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
Expand Down Expand Up @@ -408,111 +416,4 @@ private function expandParentFqns(array $fqns) : Generator
}
}
}

/**
* 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 PhpParser\FunctionLike)) {
// Walk siblings before the node
$sibling = $level;
while ($sibling = $sibling->getPreviousSibling()) {
// Collect all variables inside the sibling node
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
$vars[$var->getName()] = $var;
}
}
$level = $level->parent;
}

// If the traversal ended because a function was met,
// also add its parameters and closure uses to the result list
if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
foreach ($level->parameters->getValues() as $param) {
$paramName = $param->getName();
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
$vars[$paramName] = $param;
}
}

if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null &&
$level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
$vars[$useName] = $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\Expression\Variable[]
*/
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
{
$vars = [];
// If the child node is a variable assignment, save it

$isAssignmentToVariable = function ($node) {
return $node instanceof Node\Expression\AssignmentExpression;
};

if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
$vars[] = $node->leftOperand;
} elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
foreach ($node->getDescendantNodes() as $descendantNode) {
if ($descendantNode instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
) {
$vars[] = $descendantNode;
}
}
} else {
// Get all descendent variables, then filter to ones that start with $namePrefix.
// Avoiding closure usage in tight loop
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
$vars[] = $descendantNode->leftOperand;
}
}
}

return $vars;
}

private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
{
return $node instanceof Node\Expression\AssignmentExpression
&& $node->leftOperand instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
}
}
Loading