refactor(completion): rewrite global name completion with generators
parent
98ac9ff913
commit
d1933b8332
|
@ -4,6 +4,7 @@ namespace Whatever;
|
||||||
|
|
||||||
use TestNamespace\InnerNamespace as AliasNamespace;
|
use TestNamespace\InnerNamespace as AliasNamespace;
|
||||||
|
|
||||||
AliasNamespace\I
|
|
||||||
|
|
||||||
class IDontShowUpInCompletion {}
|
class IDontShowUpInCompletion {}
|
||||||
|
|
||||||
|
AliasNamespace\I;
|
||||||
|
AliasNamespace\;
|
||||||
|
|
|
@ -18,6 +18,13 @@ use Microsoft\PhpParser;
|
||||||
use Microsoft\PhpParser\Node;
|
use Microsoft\PhpParser\Node;
|
||||||
use Microsoft\PhpParser\ResolvedName;
|
use Microsoft\PhpParser\ResolvedName;
|
||||||
use Generator;
|
use Generator;
|
||||||
|
use function LanguageServer\FqnUtilities\{
|
||||||
|
nameConcat,
|
||||||
|
nameGetFirstPart,
|
||||||
|
nameGetParent,
|
||||||
|
nameStartsWith,
|
||||||
|
nameWithoutFirstPart
|
||||||
|
};
|
||||||
|
|
||||||
class CompletionProvider
|
class CompletionProvider
|
||||||
{
|
{
|
||||||
|
@ -280,224 +287,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;
|
||||||
|
|
||||||
$filterNameTokens = static function ($tokens) {
|
if ($nameNode instanceof Node\QualifiedName) {
|
||||||
return array_values(
|
/** @var string The typed name. */
|
||||||
array_filter(
|
$prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents());
|
||||||
$tokens,
|
} else {
|
||||||
static function ($token): bool {
|
$prefix = $nameNode->getText($node->getFileContents());
|
||||||
return $token->kind === PhpParser\TokenKind::Name;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @var string[] The written name, exploded by \ */
|
|
||||||
$prefix = array_map(
|
|
||||||
static function ($part) use ($node) : string {
|
|
||||||
return $part->getText($node->getFileContents());
|
|
||||||
},
|
|
||||||
$filterNameTokens(
|
|
||||||
$nameNode instanceof Node\QualifiedName
|
|
||||||
? $nameNode->nameParts
|
|
||||||
: [$nameNode]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($prefix === ['']) {
|
|
||||||
$prefix = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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();
|
||||||
|
|
||||||
// Get the namespace use statements
|
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
|
||||||
// TODO: use function statements, use const statements
|
$isFullyQualified = false;
|
||||||
|
|
||||||
/** @var string[] $aliases A map from local alias to fully qualified name */
|
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
|
||||||
list($aliases,,) = $node->getImportTablesForCurrentScope();
|
$isQualified = false;
|
||||||
|
|
||||||
/** @var array Array of [fqn=string, requiresRoaming=bool] the prefix may represent. */
|
if ($nameNode instanceof Node\QualifiedName) {
|
||||||
$possibleFqns = [];
|
$isFullyQualified = $nameNode->isFullyQualifiedName();
|
||||||
|
$isQualified = $nameNode->isQualifiedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var bool Whether we are in a new expression */
|
||||||
|
$isCreation = isset($creation);
|
||||||
|
|
||||||
|
/** @var array Import (use) tables */
|
||||||
|
$importTables = $node->getImportTablesForCurrentScope();
|
||||||
|
|
||||||
if ($isFullyQualified) {
|
if ($isFullyQualified) {
|
||||||
// Case \Microsoft\PhpParser\Res|
|
// \Prefix\Goes\Here| - Only return completions from the root namespace.
|
||||||
$possibleFqns[] = [$prefix, false];
|
/** @var $items \Generator|CompletionItem[] Generator yielding CompletionItems indexed by their FQN */
|
||||||
} else if ($fqnAfterAlias = $this->tryApplyAlias($aliases, $prefix)) {
|
$items = $this->getCompletionsForFqnPrefix($prefix, $isCreation, false);
|
||||||
// Cases handled here: (i.e. all namespaces involving use clauses)
|
} else if ($isQualified) {
|
||||||
//
|
// Prefix\Goes\Here|
|
||||||
// use Microsoft\PhpParser\Node; //Note that Node is both a class and a namespace.
|
$items = $this->getPartiallyQualifiedCompletions(
|
||||||
// Nod|
|
$prefix,
|
||||||
// Node\Qual|
|
$currentNamespace,
|
||||||
//
|
$importTables,
|
||||||
// use Microsoft\PhpParser as TheParser;
|
$isCreation
|
||||||
// TheParser\Nod|
|
|
||||||
$possibleFqns[] = [$fqnAfterAlias, false];
|
|
||||||
} else if ($namespaceNode) {
|
|
||||||
// Cases handled here:
|
|
||||||
//
|
|
||||||
// namespace Foo;
|
|
||||||
// Microsoft\PhpParser\Nod| // Can refer only to \Foo\Microsoft, not to \Microsoft.
|
|
||||||
//
|
|
||||||
// namespace Foo;
|
|
||||||
// Test| // Can refer either to functions or constants at the global scope, or to
|
|
||||||
// // everything below \Foo. (Global fallback / roaming)
|
|
||||||
/** @var \Microsoft\PhpParser\ResolvedName Declared namespace of the file (or section) */
|
|
||||||
$namespacedFqn = array_merge(
|
|
||||||
array_map(
|
|
||||||
static function ($token) use ($namespaceNode): string {
|
|
||||||
return $token->getText($namespaceNode->getFileContents());
|
|
||||||
},
|
|
||||||
$filterNameTokens($namespaceNode->name->nameParts)
|
|
||||||
),
|
|
||||||
$prefix
|
|
||||||
);
|
);
|
||||||
$possibleFqns[] = [$namespacedFqn, false];
|
|
||||||
if (!$isQualified) {
|
|
||||||
// Case of global fallback. If nothing is entered, also complete for root-level classnames.
|
|
||||||
// If something has been entered, complete root-level roamed symbols only.
|
|
||||||
$possibleFqns[] = [$prefix, !empty($prefix)];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Case handled here: (no namespace declaration in file)
|
// PrefixGoesHere|
|
||||||
//
|
$items = $this->getUnqualifiedCompletions($prefix, $currentNamespace, $importTables, $isCreation);
|
||||||
// Microsoft\PhpParser\N|
|
|
||||||
$possibleFqns[] = [$prefix, false];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$prefixStr = implode('\\', $prefix);
|
$list->items = array_values(iterator_to_array($items));
|
||||||
/** @var int Length of $prefix */
|
foreach ($list->items as $item) {
|
||||||
$prefixLen = strlen($prefixStr);
|
// Remove ()
|
||||||
|
if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
|
||||||
// If there is a prefix that does not contain a slash, suggest used names.
|
$item->insertText = substr($item->insertText, 0, strlen($item->insertText) - 2);
|
||||||
if (!$isQualified) {
|
|
||||||
foreach ($aliases as $alias => $fqn) {
|
|
||||||
// Suggest symbols that have been `use`d and match the prefix
|
|
||||||
if (substr($alias, 0, $prefixLen) === $prefixStr
|
|
||||||
&& ($def = $this->index->getDefinition((string)$fqn))) {
|
|
||||||
$list->items[] = CompletionItem::fromDefinition($def);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($possibleFqns as list ($fqnToSearch, $requiresRoaming)) {
|
|
||||||
$namespaceToSearch = $fqnToSearch;
|
|
||||||
array_pop($namespaceToSearch);
|
|
||||||
$namespaceToSearch = implode('\\', $namespaceToSearch);
|
|
||||||
$fqnToSearch = implode('\\', $fqnToSearch);
|
|
||||||
$fqnToSearchLen = strlen($fqnToSearch);
|
|
||||||
foreach ($this->index->getChildDefinitionsForFqn($namespaceToSearch) as $fqn => $def) {
|
|
||||||
if (isset($creation) && !$def->canBeInstantiated) {
|
|
||||||
// Only suggest classes for `new`
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if ($requiresRoaming && !$def->roamed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (substr($fqn, 0, $fqnToSearchLen) === $fqnToSearch) {
|
|
||||||
$item = CompletionItem::fromDefinition($def);
|
|
||||||
if (($aliasMatch = $this->tryMatchAlias($aliases, $fqn)) !== null) {
|
|
||||||
$item->insertText = $aliasMatch;
|
|
||||||
} else if ($namespaceNode && (empty($prefix) || $requiresRoaming)) {
|
|
||||||
// Insert the global FQN with a leading backslash.
|
|
||||||
// For empty prefix: Assume that the user wants an FQN. They have not
|
|
||||||
// started writing anything yet, so we are not second-guessing.
|
|
||||||
// For roaming: Second-guess that the user doesn't want to depend on
|
|
||||||
// roaming.
|
|
||||||
$item->insertText = '\\' . $fqn;
|
|
||||||
} else {
|
|
||||||
// Insert the FQN without a leading backslash
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggest keywords
|
|
||||||
if (!$isQualified && !isset($creation)) {
|
|
||||||
foreach (self::KEYWORDS as $keyword) {
|
|
||||||
if (substr($keyword, 0, $prefixLen) === $prefixStr) {
|
|
||||||
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
|
|
||||||
$item->insertText = $keyword;
|
|
||||||
$list->items[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function tryMatchAlias(
|
private function getPartiallyQualifiedCompletions(
|
||||||
array $aliases,
|
string $prefix,
|
||||||
string $fullyQualifiedName
|
string $currentNamespace,
|
||||||
): ?string {
|
array $importTables,
|
||||||
$fullyQualifiedName = explode('\\', $fullyQualifiedName);
|
bool $requireCanBeInstantiated
|
||||||
$aliasMatch = null;
|
): \Generator {
|
||||||
$aliasMatchLength = null;
|
// If the first part of the partially qualified name matches a namespace alias,
|
||||||
foreach ($aliases as $alias => $aliasFqn) {
|
// only definitions below that alias can be completed.
|
||||||
$aliasFqn = $aliasFqn->getNameParts();
|
list($namespaceAliases,,) = $importTables;
|
||||||
$aliasFqnLength = count($aliasFqn);
|
$prefixFirstPart = nameGetFirstPart($prefix);
|
||||||
if ($aliasMatchLength && $aliasFqnLength < $aliasFqnLength) {
|
$foundAlias = $foundAliasFqn = null;
|
||||||
// Find the longest possible match. This one won't do.
|
foreach ($namespaceAliases as $alias => $aliasFqn) {
|
||||||
continue;
|
if (strcasecmp($prefixFirstPart, $alias) === 0) {
|
||||||
}
|
$foundAlias = $alias;
|
||||||
$fqnStart = array_slice($fullyQualifiedName, 0, $aliasFqnLength);
|
$foundAliasFqn = (string)$aliasFqn;
|
||||||
if ($fqnStart === $aliasFqn) {
|
break;
|
||||||
$aliasMatch = $alias;
|
|
||||||
$aliasMatchLength = $aliasFqnLength;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($aliasMatch === null) {
|
if ($foundAlias !== null) {
|
||||||
return null;
|
yield from $this->getCompletionsFromAliasedNamespace(
|
||||||
|
$prefix,
|
||||||
|
$foundAlias,
|
||||||
|
$foundAliasFqn,
|
||||||
|
$requireCanBeInstantiated
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
yield from $this->getCompletionsForFqnPrefix(
|
||||||
|
nameConcat($currentNamespace, $prefix),
|
||||||
|
$requireCanBeInstantiated,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$fqnNoAlias = array_slice($fullyQualifiedName, $aliasMatchLength);
|
|
||||||
return join('\\', array_merge([$aliasMatch], $fqnNoAlias));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to convert a partially qualified name to an FQN using aliases.
|
* Yields completions for non-qualified global names.
|
||||||
*
|
*
|
||||||
* Example:
|
* 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)
|
||||||
*
|
*
|
||||||
* use Microsoft\PhpParser as TheParser;
|
* @return \Generator|CompletionItem[]
|
||||||
* "TheParser\Node" will convert to "Microsoft\PhpParser\Node"
|
* 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 \Microsoft\PhpParser\ResolvedName[] $aliases
|
* @param string $prefix Prefix to complete for. Fully qualified.
|
||||||
* Aliases available in the scope of resolution. Keyed by alias.
|
* @param bool $requireCanBeInstantiated If set, only return classes.
|
||||||
* @param string[] $partiallyQualifiedName
|
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
|
||||||
**/
|
* @return \Generator|CompletionItem[]
|
||||||
private function tryApplyAlias(
|
* 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 = CompletionItem::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,
|
array $aliases,
|
||||||
array $partiallyQualifiedName
|
bool $requireCanBeInstantiated
|
||||||
): ?array {
|
): \Generator {
|
||||||
if (empty($partiallyQualifiedName)) {
|
foreach ($aliases as $alias => $aliasFqn) {
|
||||||
return null;
|
if (!nameStartsWith($alias, $prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$definition = $this->index->getDefinition((string)$aliasFqn);
|
||||||
|
if ($definition) {
|
||||||
|
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$completionItem = CompletionItem::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 = CompletionItem::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;
|
||||||
}
|
}
|
||||||
$head = $partiallyQualifiedName[0];
|
|
||||||
$tail = array_slice($partiallyQualifiedName, 1);
|
|
||||||
if (!isset($aliases[$head])) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return array_merge($aliases[$head]->getNameParts(), $tail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,3 +29,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;
|
||||||
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
@ -213,11 +231,17 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
@ -236,20 +260,30 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUsedNamespace()
|
/**
|
||||||
|
* Tests completion at `AliasNamespace\I|` with `use TestNamespace\InnerNamespace as AliasNamespace`
|
||||||
|
*/
|
||||||
|
public function testUsedNamespaceWithPrefix()
|
||||||
{
|
{
|
||||||
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_namespace.php');
|
||||||
$this->loader->open($completionUri, file_get_contents($completionUri));
|
$this->loader->open($completionUri, file_get_contents($completionUri));
|
||||||
$items = $this->textDocument->completion(
|
$items = $this->textDocument->completion(
|
||||||
new TextDocumentIdentifier($completionUri),
|
new TextDocumentIdentifier($completionUri),
|
||||||
new Position(6, 16)
|
new Position(8, 16)
|
||||||
)->wait();
|
)->wait();
|
||||||
$this->assertCompletionsListSubset(new CompletionList([
|
$this->assertEquals(
|
||||||
|
new CompletionList([
|
||||||
new CompletionItem(
|
new CompletionItem(
|
||||||
'InnerClass',
|
'InnerClass',
|
||||||
CompletionItemKind::CLASS_,
|
CompletionItemKind::CLASS_,
|
||||||
|
@ -259,9 +293,41 @@ class CompletionTest extends TestCase
|
||||||
null,
|
null,
|
||||||
'AliasNamespace\\InnerClass'
|
'AliasNamespace\\InnerClass'
|
||||||
)
|
)
|
||||||
], true), $items);
|
], 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');
|
||||||
|
@ -860,6 +941,9 @@ class CompletionTest extends TestCase
|
||||||
], 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');
|
||||||
|
|
Loading…
Reference in New Issue