From 44d26ba1aa90616d0cbcd359d94bda198984fb3e Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 19 Nov 2016 05:06:24 +0100 Subject: [PATCH 01/25] Add support for method/property completion --- fixtures/completion.php | 4 ++ src/DefinitionResolver.php | 2 +- src/LanguageServer.php | 7 ++- src/Protocol/CompletionItem.php | 57 +++++++++++++++++ src/Protocol/CompletionItemKind.php | 2 +- src/Protocol/CompletionOptions.php | 4 +- src/Server/TextDocument.php | 66 +++++++++++++++++++- tests/LanguageServerTest.php | 9 ++- tests/Server/TextDocument/CompletionTest.php | 55 ++++++++++++++++ 9 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 fixtures/completion.php create mode 100644 tests/Server/TextDocument/CompletionTest.php diff --git a/fixtures/completion.php b/fixtures/completion.php new file mode 100644 index 00000000..677bcbbc --- /dev/null +++ b/fixtures/completion.php @@ -0,0 +1,4 @@ +t diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index a35750ee..64ae2475 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -327,7 +327,7 @@ public static function resolveVariableToNode(Node\Expr\Variable $var) * @param \PhpParser\Node\Expr $expr * @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->name === 'this') { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index a2c1f524..7f884941 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -11,7 +11,8 @@ MessageType, InitializeResult, SymbolInformation, - TextDocumentIdentifier + TextDocumentIdentifier, + CompletionOptions }; use AdvancedJsonRpc; use Sabre\Event\{Loop, Promise}; @@ -140,6 +141,10 @@ public function initialize(int $processId, ClientCapabilities $capabilities, str $serverCapabilities->referencesProvider = true; // Support "Hover" $serverCapabilities->hoverProvider = true; + // Support "Completion" + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; return new InitializeResult($serverCapabilities); } diff --git a/src/Protocol/CompletionItem.php b/src/Protocol/CompletionItem.php index 780f31f4..75fbeded 100644 --- a/src/Protocol/CompletionItem.php +++ b/src/Protocol/CompletionItem.php @@ -69,6 +69,24 @@ class CompletionItem */ public $textEdit; + /** + * An optional array of additional text edits that are applied when + * selecting this completion. Edits must not overlap with the main edit + * nor with themselves. + * + * @var TextEdit[]|null + */ + public $additionalTextEdits; + + /** + * An optional command that is executed *after* inserting this completion. *Note* that + * additional modifications to the current document should be described with the + * additionalTextEdits-property. + * + * @var Command|null + */ + public $command; + /** * An data entry field that is preserved on a completion item between * a completion and a completion resolve request. @@ -76,4 +94,43 @@ class CompletionItem * @var mixed */ public $data; + + /** + * @param string $label + * @param int|null $kind + * @param string|null $detail + * @param string|null $documentation + * @param string|null $sortText + * @param string|null $filterText + * @param string|null $insertQuery + * @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 $insertQuery = 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->insertQuery = $insertQuery; + $this->textEdit = $textEdit; + $this->additionalTextEdits = $additionalTextEdits; + $this->command = $command; + $this->data = $data; + } } diff --git a/src/Protocol/CompletionItemKind.php b/src/Protocol/CompletionItemKind.php index 6ef57963..3c4e72f0 100644 --- a/src/Protocol/CompletionItemKind.php +++ b/src/Protocol/CompletionItemKind.php @@ -13,7 +13,7 @@ abstract class CompletionItemKind const CONSTRUCTOR = 4; const FIELD = 5; const VARIABLE = 6; - const _CLASS = 7; + const CLASS_ = 7; const INTERFACE = 8; const MODULE = 9; const PROPERTY = 10; diff --git a/src/Protocol/CompletionOptions.php b/src/Protocol/CompletionOptions.php index 0be727ed..f668ca0c 100644 --- a/src/Protocol/CompletionOptions.php +++ b/src/Protocol/CompletionOptions.php @@ -11,14 +11,14 @@ class CompletionOptions * The server provides support to resolve additional information for a completion * item. * - * @var bool + * @var bool|null */ public $resolveProvider; /** * The characters that trigger completion automatically. * - * @var string|null + * @var string[]|null */ public $triggerCharacters; } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 6c673888..a04407dc 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -18,8 +18,12 @@ SymbolInformation, ReferenceContext, Hover, - MarkedString + MarkedString, + SymbolKind, + CompletionItem, + CompletionItemKind }; +use phpDocumentor\Reflection\Types; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -210,4 +214,64 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position): 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 + */ + public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise + { + return coroutine(function () use ($textDocument, $position) { + $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $node = $document->getNodeAtPosition($position); + if ($node === null) { + return []; + } + if ($node instanceof Node\Expr\Error) { + $node = $node->getAttribute('parentNode'); + } + if ($node instanceof Node\Expr\PropertyFetch) { + // Resolve object + $objType = $this->definitionResolver->resolveExpressionNodeToType($node->var); + if ($objType instanceof Types\Object_ && $objType->getFqsen() !== null) { + $prefix = substr((string)$objType->getFqsen(), 1) . '::'; + if (is_string($node->name)) { + $prefix .= $node->name; + } + $prefixLen = strlen($prefix); + $items = []; + foreach ($this->project->getDefinitions() as $fqn => $def) { + if (substr($fqn, 0, $prefixLen) === $prefix) { + $item = new CompletionItem; + $item->label = $def->symbolInformation->name; + if ($def->type) { + $item->detail = (string)$def->type; + } + if ($def->documentation) { + $item->documentation = $def->documentation; + } + if ($def->symbolInformation->kind === SymbolKind::PROPERTY) { + $item->kind = CompletionItemKind::PROPERTY; + } else if ($def->symbolInformation->kind === SymbolKind::METHOD) { + $item->kind = CompletionItemKind::METHOD; + } + $items[] = $item; + } + } + return $items; + } + } + return []; + }); + } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 272c458a..98d5d362 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -36,7 +36,10 @@ public function testInitialize() 'textDocumentSync' => TextDocumentSyncKind::FULL, 'documentSymbolProvider' => true, 'hoverProvider' => true, - 'completionProvider' => null, + 'completionProvider' => (object)[ + 'resolveProvider' => false, + 'triggerCharacters' => ['$', '>'] + ], 'signatureHelpProvider' => null, 'definitionProvider' => true, 'referencesProvider' => true, @@ -61,7 +64,7 @@ public function testIndexingWithDirectFileAccess() if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $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 11 PHP files parsed') !== false) { $promise->fulfill(); } } @@ -106,7 +109,7 @@ public function testIndexingWithFilesAndContentRequests() if ($promise->state === Promise::PENDING) { $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 11 PHP files parsed') !== false) { // Indexing finished $promise->fulfill(); } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php new file mode 100644 index 00000000..5aee8f27 --- /dev/null +++ b/tests/Server/TextDocument/CompletionTest.php @@ -0,0 +1,55 @@ +completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion.php'); + $project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php')); + $project->openDocument($this->completionUri, file_get_contents($this->completionUri)); + $this->textDocument = new Server\TextDocument($project, $client); + } + + public function testCompletion() + { + $items = $this->textDocument->completion( + new TextDocumentIdentifier($this->completionUri), + new Position(3, 7) + )->wait(); + $this->assertEquals([ + 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.' + ) + ], $items); + } +} From 5125fa748eb213bc956e06f86ffb2ba27e9ffc76 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 19 Nov 2016 14:02:43 +0100 Subject: [PATCH 02/25] Move completion fixtures into directory --- .../property.php} | 0 tests/Server/TextDocument/CompletionTest.php | 18 +++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) rename fixtures/{completion.php => completion/property.php} (100%) diff --git a/fixtures/completion.php b/fixtures/completion/property.php similarity index 100% rename from fixtures/completion.php rename to fixtures/completion/property.php diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 5aee8f27..800d88c3 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -17,24 +17,24 @@ class CompletionTest extends TestCase private $textDocument; /** - * @var string + * @var Project */ - private $completionUri; + private $project; public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new ClientCapabilities); - $this->completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion.php'); - $project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php')); - $project->openDocument($this->completionUri, file_get_contents($this->completionUri)); - $this->textDocument = new Server\TextDocument($project, $client); + $this->project = new Project($client, new ClientCapabilities); + $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); + $this->textDocument = new Server\TextDocument($this->project, $client); } - public function testCompletion() + public function testForPropertiesAndMethods() { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); + $this->project->openDocument($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( - new TextDocumentIdentifier($this->completionUri), + new TextDocumentIdentifier($completionUri), new Position(3, 7) )->wait(); $this->assertEquals([ From 59670af7bdf25c6d75ccabdf0fe6bd84e67ee0d2 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 20 Nov 2016 18:53:03 +0100 Subject: [PATCH 03/25] Add support for variable suggestions Refactor logic into CompletionProvider class --- fixtures/completion/variable.php | 10 + src/CompletionProvider.php | 198 +++++++++++++++++++ src/DefinitionResolver.php | 23 ++- src/Server/TextDocument.php | 49 +---- tests/LanguageServerTest.php | 4 +- tests/Server/TextDocument/CompletionTest.php | 14 ++ 6 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 fixtures/completion/variable.php create mode 100644 src/CompletionProvider.php diff --git a/fixtures/completion/variable.php b/fixtures/completion/variable.php new file mode 100644 index 00000000..483f5dc8 --- /dev/null +++ b/fixtures/completion/variable.php @@ -0,0 +1,10 @@ +definitionResolver = $definitionResolver; + $this->project = $project; + } + + /** + * Returns suggestions for a specific cursor position in a document + * + * @param PhpDocument $document The opened document + * @param Position $position The cursor position + * @return CompletionItem[] + */ + public function provideCompletion(PhpDocument $document, Position $position): array + { + $node = $document->getNodeAtPosition($position); + + /** @var CompletionItem[] */ + $items = []; + + if ($node instanceof Node\Expr\Error) { + $node = $node->getAttribute('parentNode'); + } + + // If we get a property fetch node, resolve items of the class + if ($node instanceof Node\Expr\PropertyFetch) { + $objType = $this->definitionResolver->resolveExpressionNodeToType($node->var); + if ($objType instanceof Types\Object_ && $objType->getFqsen() !== null) { + $prefix = substr((string)$objType->getFqsen(), 1) . '::'; + if (is_string($node->name)) { + $prefix .= $node->name; + } + $prefixLen = strlen($prefix); + foreach ($this->project->getDefinitions() as $fqn => $def) { + if (substr($fqn, 0, $prefixLen) === $prefix) { + $item = new CompletionItem; + $item->label = $def->symbolInformation->name; + if ($def->type) { + $item->detail = (string)$def->type; + } + if ($def->documentation) { + $item->documentation = $def->documentation; + } + if ($def->symbolInformation->kind === SymbolKind::PROPERTY) { + $item->kind = CompletionItemKind::PROPERTY; + } else if ($def->symbolInformation->kind === SymbolKind::METHOD) { + $item->kind = CompletionItemKind::METHOD; + } + $items[] = $item; + } + } + } + } else { + // Find variables, parameters and use statements in the scope + foreach ($this->suggestVariablesAtNode($node) as $var) { + $item = new CompletionItem; + $item->kind = CompletionItemKind::VARIABLE; + $item->documentation = $this->definitionResolver->getDocumentationFromNode($var); + if ($var instanceof Node\Param) { + $item->label = '$' . $var->name; + $item->detail = (string)$this->definitionResolver->getTypeFromNode($var); // TODO make it handle variables as well. Makes sense because needs to handle @var tag too! + } else if ($var instanceof Node\Expr\Variable || $var instanceof Node\Expr\ClosureUse) { + $item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name); + $item->detail = (string)$this->definitionResolver->resolveExpressionNodeToType($var->getAttribute('parentNode')); + } else { + throw new \LogicException; + } + $items[] = $item; + } + } + + return $items; + } + + /** + * 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 + * @return array + */ + private function suggestVariablesAtNode(Node $node): 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) 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) 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])) { + $vars[$param->name] = $param; + } + } + if ($level instanceof Node\Expr\Closure) { + foreach ($level->uses as $use) { + if (!isset($vars[$param->name])) { + $vars[$use->var] = $use; + } + } + } + } + + return array_values($vars); + } + + /** + * Searches the subnodes of a node for variable assignments + * + * @param Node $node + * @return Node\Expr\Variable[] + */ + private function findVariableDefinitionsInNode(Node $node): 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 + ) { + $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) as $var) { + $vars[] = $var; + } + } + } + return $vars; + } +} diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 64ae2475..677cf894 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -281,25 +281,34 @@ private static function getContainingClassFqn(Node $node) /** * 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 */ - public static function resolveVariableToNode(Node\Expr\Variable $var) + public static function resolveVariableToNode(Node\Expr $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 do { // If a function is met, check the parameters and use statements if ($n instanceof Node\FunctionLike) { foreach ($n->getParams() as $param) { - if ($param->name === $var->name) { + if ($param->name === $name) { return $param; } } // If it is a closure, also check use statements if ($n instanceof Node\Expr\Closure) { foreach ($n->uses as $use) { - if ($use->var === $var->name) { + if ($use->var === $name) { return $use; } } @@ -310,7 +319,7 @@ public static function resolveVariableToNode(Node\Expr\Variable $var) while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) { if ( ($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; } @@ -329,8 +338,8 @@ public static function resolveVariableToNode(Node\Expr\Variable $var) */ public function resolveExpressionNodeToType(Node\Expr $expr): Type { - if ($expr instanceof Node\Expr\Variable) { - if ($expr->name === 'this') { + if ($expr instanceof Node\Expr\Variable || $expr instanceof Node\Expr\ClosureUse) { + if ($expr instanceof Node\Expr\Variable && $expr->name === 'this') { return new Types\This; } // Find variable definition diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index a04407dc..5998accc 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -3,7 +3,7 @@ namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver}; +use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider}; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use PhpParser\Node; use LanguageServer\Protocol\{ @@ -23,7 +23,6 @@ CompletionItem, CompletionItemKind }; -use phpDocumentor\Reflection\Types; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -54,12 +53,18 @@ class TextDocument */ private $definitionResolver; + /** + * @var CompletionProvider + */ + private $completionProvider; + public function __construct(Project $project, LanguageClient $client) { $this->project = $project; $this->client = $client; $this->prettyPrinter = new PrettyPrinter(); $this->definitionResolver = new DefinitionResolver($project); + $this->completionProvider = new CompletionProvider($this->definitionResolver, $project); } /** @@ -233,45 +238,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit { return coroutine(function () use ($textDocument, $position) { $document = yield $this->project->getOrLoadDocument($textDocument->uri); - $node = $document->getNodeAtPosition($position); - if ($node === null) { - return []; - } - if ($node instanceof Node\Expr\Error) { - $node = $node->getAttribute('parentNode'); - } - if ($node instanceof Node\Expr\PropertyFetch) { - // Resolve object - $objType = $this->definitionResolver->resolveExpressionNodeToType($node->var); - if ($objType instanceof Types\Object_ && $objType->getFqsen() !== null) { - $prefix = substr((string)$objType->getFqsen(), 1) . '::'; - if (is_string($node->name)) { - $prefix .= $node->name; - } - $prefixLen = strlen($prefix); - $items = []; - foreach ($this->project->getDefinitions() as $fqn => $def) { - if (substr($fqn, 0, $prefixLen) === $prefix) { - $item = new CompletionItem; - $item->label = $def->symbolInformation->name; - if ($def->type) { - $item->detail = (string)$def->type; - } - if ($def->documentation) { - $item->documentation = $def->documentation; - } - if ($def->symbolInformation->kind === SymbolKind::PROPERTY) { - $item->kind = CompletionItemKind::PROPERTY; - } else if ($def->symbolInformation->kind === SymbolKind::METHOD) { - $item->kind = CompletionItemKind::METHOD; - } - $items[] = $item; - } - } - return $items; - } - } - return []; + return $this->completionProvider->provideCompletion($document, $position); }); } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 98d5d362..6892f8fa 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -64,7 +64,7 @@ public function testIndexingWithDirectFileAccess() if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); - } else if (strpos($msg->body->params->message, 'All 11 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 12 PHP files parsed') !== false) { $promise->fulfill(); } } @@ -109,7 +109,7 @@ public function testIndexingWithFilesAndContentRequests() if ($promise->state === Promise::PENDING) { $promise->reject(new Exception($msg->body->params->message)); } - } else if (strpos($msg->body->params->message, 'All 11 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 12 PHP files parsed') !== false) { // Indexing finished $promise->fulfill(); } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 800d88c3..e493488f 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -52,4 +52,18 @@ public function testForPropertiesAndMethods() ) ], $items); } + + public function testForVariables() + { + $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 CompletionItem('$var', CompletionItemKind::VARIABLE, 'int'), + new CompletionItem('$param', CompletionItemKind::VARIABLE, 'string|null', 'A parameter') + ], $items); + } } From 49b526d7e129d4977c965a6a5b3847dd51a6b1bd Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 20 Nov 2016 23:33:00 +0100 Subject: [PATCH 04/25] Allow getTypeFromNode() to take Variable nodes --- src/CompletionProvider.php | 11 ++--------- src/DefinitionResolver.php | 35 +++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 80604fe1..b51e652b 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -85,16 +85,9 @@ public function provideCompletion(PhpDocument $document, Position $position): ar foreach ($this->suggestVariablesAtNode($node) 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); - if ($var instanceof Node\Param) { - $item->label = '$' . $var->name; - $item->detail = (string)$this->definitionResolver->getTypeFromNode($var); // TODO make it handle variables as well. Makes sense because needs to handle @var tag too! - } else if ($var instanceof Node\Expr\Variable || $var instanceof Node\Expr\ClosureUse) { - $item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name); - $item->detail = (string)$this->definitionResolver->resolveExpressionNodeToType($var->getAttribute('parentNode')); - } else { - throw new \LogicException; - } + $item->detail = (string)$this->definitionResolver->getTypeFromNode($var); $items[] = $item; } } diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 677cf894..9f20b1bc 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -113,13 +113,8 @@ public function resolveReferenceNodeToDefinition(Node $node) $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); - } + // Get type from docblock + $def->type = $this->getTypeFromNode($defNode); return $def; } // Other references are references to a global symbol that have an FQN @@ -608,7 +603,7 @@ private static function resolveClassNameToType(Node $class): Type * For functions and methods, this is the return type. * For parameters, this is the type of the parameter. * 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. * If it is unknown, will be Types\Mixed. * Returns null if the node does not have a type. @@ -666,16 +661,28 @@ public function getTypeFromNode(Node $node) // Unknown return type return new Types\Mixed; } - if ($node instanceof Node\Stmt\PropertyProperty || $node instanceof Node\Const_) { - // Property or constant - $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); + if ($node instanceof Node\Expr\Variable) { + $node = $node->getAttribute('parentNode'); + } + if ( + $node instanceof Node\Stmt\PropertyProperty + || $node instanceof Node\Const_ + || $node instanceof Node\Expr\Assign + || $node instanceof Node\Expr\AssignOp + ) { + // Property, constant or variable if ( - $docBlock !== null + ($parent = $node->getAttribute('parentNode')) + && ($docBlock = $parent->getAttribute('docBlock')) && !empty($varTags = $docBlock->getTagsByName('var')) - && $varTags[0]->getType() + && ($type = $varTags[0]->getType()) ) { // Use @var tag - return $varTags[0]->getType(); + return $type; + } + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { + // Resolve the expression + return $this->resolveExpressionNodeToType($node); } // TODO: read @property tags of class // TODO: Try to infer the type from default value / constant value From 4365fac0b0723d6e065905c05a7f5797b50559bc Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 20 Nov 2016 23:58:22 +0100 Subject: [PATCH 05/25] Use property and constant values --- src/DefinitionResolver.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 9f20b1bc..e8b28c5b 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -671,17 +671,23 @@ public function getTypeFromNode(Node $node) || $node instanceof Node\Expr\AssignOp ) { // Property, constant or variable + // Use @var tag if ( ($parent = $node->getAttribute('parentNode')) && ($docBlock = $parent->getAttribute('docBlock')) && !empty($varTags = $docBlock->getTagsByName('var')) && ($type = $varTags[0]->getType()) ) { - // Use @var tag return $type; } - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { - // Resolve the expression + // 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 From e6a4103f9759a2fc451413c8ccbc1c467c3e30d2 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Mon, 21 Nov 2016 01:29:38 +0100 Subject: [PATCH 06/25] Fix using @var tag for variables --- src/DefinitionResolver.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index e8b28c5b..827fb55f 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -670,11 +670,16 @@ public function getTypeFromNode(Node $node) || $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 if ( - ($parent = $node->getAttribute('parentNode')) - && ($docBlock = $parent->getAttribute('docBlock')) + isset($docBlockHolder) + && ($docBlock = $docBlockHolder->getAttribute('docBlock')) && !empty($varTags = $docBlock->getTagsByName('var')) && ($type = $varTags[0]->getType()) ) { From 06636ded54db8d1a9231177b28dc22f8f7ab5be2 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 16:12:12 +0100 Subject: [PATCH 07/25] Improve completion * classes * variables with prefix filtering --- .editorconfig | 2 +- composer.json | 2 +- fixtures/completion/fully_qualified_class.php | 9 ++ fixtures/completion/used_class.php | 9 ++ fixtures/completion/used_new.php | 7 + fixtures/completion/variable_with_prefix.php | 10 ++ src/CompletionProvider.php | 135 +++++++++++++----- src/Definition.php | 15 ++ src/DefinitionResolver.php | 35 +++-- src/NodeVisitor/DefinitionCollector.php | 9 +- src/Protocol/CompletionItem.php | 31 +++- src/Protocol/CompletionItemKind.php | 42 ++++++ tests/Server/TextDocument/CompletionTest.php | 84 ++++++++++- 13 files changed, 331 insertions(+), 59 deletions(-) create mode 100644 fixtures/completion/fully_qualified_class.php create mode 100644 fixtures/completion/used_class.php create mode 100644 fixtures/completion/used_new.php create mode 100644 fixtures/completion/variable_with_prefix.php diff --git a/.editorconfig b/.editorconfig index 55581242..d1620666 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,5 @@ indent_size = 2 [composer.json] indent_size = 4 -[*.md] +[{*.md,fixtures/**}] trim_trailing_whitespace = false diff --git a/composer.json b/composer.json index df9110aa..b29008c4 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "bin": ["bin/php-language-server.php"], "require": { "php": ">=7.0", - "nikic/php-parser": "^3.0.0beta2", + "nikic/php-parser": "dev-master#c5cdd5ad73ac20d855b84fa6d0f1f22ebff2e302", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^5.0", "felixfbecker/advanced-json-rpc": "^2.0", diff --git a/fixtures/completion/fully_qualified_class.php b/fixtures/completion/fully_qualified_class.php new file mode 100644 index 00000000..8c4da3d0 --- /dev/null +++ b/fixtures/completion/fully_qualified_class.php @@ -0,0 +1,9 @@ +getAttribute('parentNode'); - } - - // If we get a property fetch node, resolve items of the class - if ($node instanceof Node\Expr\PropertyFetch) { - $objType = $this->definitionResolver->resolveExpressionNodeToType($node->var); - if ($objType instanceof Types\Object_ && $objType->getFqsen() !== null) { - $prefix = substr((string)$objType->getFqsen(), 1) . '::'; - if (is_string($node->name)) { - $prefix .= $node->name; + // 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 + ) { + /** The FQN to be completed */ + $prefix = $this->definitionResolver->resolveReferenceNodeToFqn($node) ?? ''; + $prefixLen = strlen($prefix); + foreach ($this->project->getDefinitions() as $fqn => $def) { + if (substr($fqn, 0, $prefixLen) === $prefix && !$def->isGlobal) { + $items[] = CompletionItem::fromDefinition($def); } + } + } else if ( + // A ConstFetch means any static reference, like a class, interface, etc. + ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) + || $node instanceof Node\Expr\New_ + ) { + $prefix = null; + if ($node instanceof Node\Name) { + $isFullyQualified = $node->isFullyQualified(); + $prefix = (string)$node; $prefixLen = strlen($prefix); - foreach ($this->project->getDefinitions() as $fqn => $def) { - if (substr($fqn, 0, $prefixLen) === $prefix) { - $item = new CompletionItem; - $item->label = $def->symbolInformation->name; - if ($def->type) { - $item->detail = (string)$def->type; - } - if ($def->documentation) { - $item->documentation = $def->documentation; - } - if ($def->symbolInformation->kind === SymbolKind::PROPERTY) { - $item->kind = CompletionItemKind::PROPERTY; - } else if ($def->symbolInformation->kind === SymbolKind::METHOD) { - $item->kind = CompletionItemKind::METHOD; + $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, $use->name); + $aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); } - $items[] = $item; + } 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) { + $items[] = CompletionItem::fromDefinition($def); } } } - } else { + // 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 + || ( + ($isFullyQualified && substr($fqn, 0, $prefixLen) === $prefix) + || (!$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) { + // Insert the global FQN with trailing backslash + $item->insertText = '\\' . $fqn; + } else { + // Insert the FQN without trailing backlash + $item->insertText = $fqn; + } + $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 - foreach ($this->suggestVariablesAtNode($node) as $var) { + // 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); @@ -101,16 +163,17 @@ public function provideCompletion(PhpDocument $document, Position $position): ar * of that variable * * @param Node $node + * @param string $namePrefix Prefix to filter * @return array */ - private function suggestVariablesAtNode(Node $node): array + 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) as $var) { + foreach ($this->findVariableDefinitionsInNode($node, $namePrefix) as $var) { // Only use the first definition if (!isset($vars[$var->name])) { $vars[$var->name] = $var; @@ -124,7 +187,7 @@ private function suggestVariablesAtNode(Node $node): array $sibling = $level; while ($sibling = $sibling->getAttribute('previousSibling')) { // Collect all variables inside the sibling node - foreach ($this->findVariableDefinitionsInNode($sibling) as $var) { + foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) { $vars[$var->name] = $var; } } @@ -135,13 +198,13 @@ private function suggestVariablesAtNode(Node $node): array // 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])) { + 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])) { + if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { $vars[$use->var] = $use; } } @@ -155,9 +218,10 @@ private function suggestVariablesAtNode(Node $node): array * 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): array + private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array { $vars = []; // If the child node is a variable assignment, save it @@ -166,6 +230,7 @@ private function findVariableDefinitionsInNode(Node $node): array $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; } @@ -181,7 +246,7 @@ private function findVariableDefinitionsInNode(Node $node): array if (!($child instanceof Node) || $child instanceof Node\FunctionLike) { continue; } - foreach ($this->findVariableDefinitionsInNode($child) as $var) { + foreach ($this->findVariableDefinitionsInNode($child, $namePrefix) as $var) { $vars[] = $var; } } diff --git a/src/Definition.php b/src/Definition.php index cba69ab9..0459ff3c 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -29,6 +29,21 @@ class Definition */ 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; + + /** + * True if the Definition is a class + * + * @var bool + */ + public $canBeInstantiated; + /** * @var Protocol\SymbolInformation */ diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 827fb55f..b90d7b38 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -91,6 +91,30 @@ public function getDocumentationFromNode(Node $node) } } + /** + * 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\Function_ + || $node->getAttribute('parentNode') instanceof Node\Stmt\Const_ + ); + $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 * @@ -106,16 +130,7 @@ public function resolveReferenceNodeToDefinition(Node $node) if ($defNode === null) { return null; } - $def = new Definition; - // 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); - // Get type from docblock - $def->type = $this->getTypeFromNode($defNode); - return $def; + return $this->createDefinitionFromNode($defNode); } // Other references are references to a global symbol that have an FQN // Find out the FQN diff --git a/src/NodeVisitor/DefinitionCollector.php b/src/NodeVisitor/DefinitionCollector.php index 162f6708..51981395 100644 --- a/src/NodeVisitor/DefinitionCollector.php +++ b/src/NodeVisitor/DefinitionCollector.php @@ -42,13 +42,6 @@ public function enterNode(Node $node) return; } $this->nodes[$fqn] = $node; - $def = new Definition; - $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; + $this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn); } } diff --git a/src/Protocol/CompletionItem.php b/src/Protocol/CompletionItem.php index 75fbeded..a764175e 100644 --- a/src/Protocol/CompletionItem.php +++ b/src/Protocol/CompletionItem.php @@ -1,7 +1,10 @@ documentation = $documentation; $this->sortText = $sortText; $this->filterText = $filterText; - $this->insertQuery = $insertQuery; + $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; + } + return $item; + } } diff --git a/src/Protocol/CompletionItemKind.php b/src/Protocol/CompletionItemKind.php index 3c4e72f0..046f8ef0 100644 --- a/src/Protocol/CompletionItemKind.php +++ b/src/Protocol/CompletionItemKind.php @@ -25,4 +25,46 @@ abstract class CompletionItemKind const COLOR = 16; const FILE = 17; const REFERENCE = 18; + + /** + * Returns the CompletionItemKind for a SymbolKind + * + * @param int $kind A SymbolKind + * @return int The CompletionItemKind + */ + public static function fromSymbolKind(int $kind): int + { + switch ($kind) { + case SymbolKind::PROPERTY: + case SymbolKind::FIELD: + return self::PROPERTY; + case SymbolKind::METHOD: + return self::METHOD; + case SymbolKind::CLASS_: + return self::CLASS_; + case SymbolKind::INTERFACE: + return self::INTERFACE; + case SymbolKind::FUNCTION: + return self::FUNCTION; + case SymbolKind::NAMESPACE: + case SymbolKind::MODULE: + case SymbolKind::PACKAGE: + return self::MODULE; + case SymbolKind::FILE: + return self::FILE; + case SymbolKind::STRING: + return self::TEXT; + case SymbolKind::NUMBER: + case SymbolKind::BOOLEAN: + case SymbolKind::ARRAY: + return self::VALUE; + case SymbolKind::ENUM: + return self::ENUM; + case SymbolKind::CONSTRUCTOR: + return self::CONSTRUCTOR; + case SymbolKind::VARIABLE: + case SymbolKind::CONSTANT: + return self::VARIABLE; + } + } } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index e493488f..8f22ad03 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -26,6 +26,7 @@ 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); } @@ -53,7 +54,7 @@ public function testForPropertiesAndMethods() ], $items); } - public function testForVariables() + public function testVariable() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); $this->project->openDocument($completionUri, file_get_contents($completionUri)); @@ -66,4 +67,85 @@ public function testForVariables() new CompletionItem('$param', CompletionItemKind::VARIABLE, 'string|null', 'A parameter') ], $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, 5) + )->wait(); + $this->assertEquals([ + new CompletionItem('$param', CompletionItemKind::VARIABLE, 'string|null', 'A parameter') + ], $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([ + // 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' + ), + ], $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 CompletionItem( + 'TestClass', + CompletionItemKind::CLASS_, + 'TestNamespace', + 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' + ) + ], $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 CompletionItem( + 'TestClass', + CompletionItemKind::CLASS_, + null, + 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' + ) + ], $items); + } } From 151dea6ad1cdf705e246a7601cd98b737becdc7d Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 17:12:24 +0100 Subject: [PATCH 08/25] Make FQNs more distinct * use -> for instance methods/properties * use ::$ for static properties --- src/Definition.php | 6 ++-- src/DefinitionResolver.php | 36 ++++++++++++++----- src/Protocol/SymbolInformation.php | 2 +- tests/NodeVisitor/DefinitionCollectorTest.php | 12 +++---- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/Definition.php b/src/Definition.php index 0459ff3c..c40e8a8b 100644 --- a/src/Definition.php +++ b/src/Definition.php @@ -20,10 +20,10 @@ class Definition * - testFunction() * - TestNamespace\TestClass * - TestNamespace\TestClass::TEST_CONSTANT - * - TestNamespace\TestClass::staticTestProperty - * - TestNamespace\TestClass::testProperty + * - TestNamespace\TestClass::$staticTestProperty + * - TestNamespace\TestClass->testProperty * - TestNamespace\TestClass::staticTestMethod() - * - TestNamespace\TestClass::testMethod() + * - TestNamespace\TestClass->testMethod() * * @var string|null */ diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index b90d7b38..e9e5eacf 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -221,7 +221,7 @@ public function resolveReferenceNodeToFqn(Node $node) } else { $classFqn = substr((string)$varType->getFqsen(), 1); } - $name = $classFqn . '::' . (string)$node->name; + $name = $classFqn . '->' . (string)$node->name; } else if ($parent instanceof Node\Expr\FuncCall) { if ($parent->name instanceof Node\Expr) { return null; @@ -255,7 +255,11 @@ public function resolveReferenceNodeToFqn(Node $node) $className = (string)$classNode->namespacedName; } } - $name = (string)$className . '::' . $node->name; + if ($node instanceof Node\Expr\StaticPropertyFetch) { + $name = (string)$className . '::$' . $node->name; + } else { + $name = (string)$className . '::' . $node->name; + } } else { return null; } @@ -404,7 +408,7 @@ public function resolveExpressionNodeToType(Node\Expr $expr): Type } else { $classFqn = substr((string)$t->getFqsen(), 1); } - $fqn = $classFqn . '::' . $expr->name; + $fqn = $classFqn . '->' . $expr->name; if ($expr instanceof Node\Expr\MethodCall) { $fqn .= '()'; } @@ -423,7 +427,11 @@ public function resolveExpressionNodeToType(Node\Expr $expr): Type if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null || $expr->name instanceof Node\Expr) { 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) { $fqn .= '()'; } @@ -735,21 +743,31 @@ public static function getDefinedFqn(Node $node) // Function: use functionName() as the name return (string)$node->namespacedName . '()'; } 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'); if (!isset($class->name)) { // Ignore anonymous classes return null; } - return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + if ($node->isStatic()) { + return (string)$class->namespacedName . '::' . (string)$node->name . '()'; + } else { + return (string)$class->namespacedName . '->' . (string)$node->name . '()'; + } } else if ($node instanceof Node\Stmt\PropertyProperty) { - // Property: use ClassName::propertyName as name - $class = $node->getAttribute('parentNode')->getAttribute('parentNode'); + $property = $node->getAttribute('parentNode'); + $class = $property->getAttribute('parentNode'); if (!isset($class->name)) { // Ignore anonymous classes 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_) { $parent = $node->getAttribute('parentNode'); if ($parent instanceof Node\Stmt\Const_) { diff --git a/src/Protocol/SymbolInformation.php b/src/Protocol/SymbolInformation.php index 1111dc08..06e8f7e9 100644 --- a/src/Protocol/SymbolInformation.php +++ b/src/Protocol/SymbolInformation.php @@ -88,7 +88,7 @@ public static function fromNode(Node $node, string $fqn = null) } $symbol->location = Location::fromNode($node); if ($fqn !== null) { - $parts = preg_split('/(::|\\\\)/', $fqn); + $parts = preg_split('/(::|->|\\\\)/', $fqn); array_pop($parts); $symbol->containerName = implode('\\', $parts); } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index ded65d1c..6768c94c 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -33,10 +33,10 @@ public function testCollectsSymbols() 'TestNamespace\\TEST_CONST', 'TestNamespace\\TestClass', 'TestNamespace\\TestClass::TEST_CLASS_CONST', - 'TestNamespace\\TestClass::staticTestProperty', - 'TestNamespace\\TestClass::testProperty', + 'TestNamespace\\TestClass::$staticTestProperty', + 'TestNamespace\\TestClass->testProperty', 'TestNamespace\\TestClass::staticTestMethod()', - 'TestNamespace\\TestClass::testMethod()', + 'TestNamespace\\TestClass->testMethod()', 'TestNamespace\\TestTrait', 'TestNamespace\\TestInterface', 'TestNamespace\\test_function()' @@ -44,10 +44,10 @@ public function testCollectsSymbols() $this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']); $this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']); $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::testProperty']); + $this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']); + $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::testMethod()']); + $this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass->testMethod()']); $this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']); $this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']); From 51de0b5dfceb980a67bfdfdfa13ea1de32c16490 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 17:33:56 +0100 Subject: [PATCH 09/25] Add tests for static access --- .../completion/class_const_with_prefix.php | 3 ++ .../completion/static_method_with_prefix.php | 3 ++ .../static_property_with_prefix.php | 3 ++ src/PhpDocument.php | 3 ++ tests/Server/TextDocument/CompletionTest.php | 54 +++++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 fixtures/completion/class_const_with_prefix.php create mode 100644 fixtures/completion/static_method_with_prefix.php create mode 100644 fixtures/completion/static_property_with_prefix.php diff --git a/fixtures/completion/class_const_with_prefix.php b/fixtures/completion/class_const_with_prefix.php new file mode 100644 index 00000000..36c71d4a --- /dev/null +++ b/fixtures/completion/class_const_with_prefix.php @@ -0,0 +1,3 @@ +stmts === null) { + return null; + } $traverser = new NodeTraverser; $finder = new NodeAtPositionFinder($position); $traverser->addVisitor($finder); diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 8f22ad03..3b57c826 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -131,6 +131,60 @@ public function testUsedClass() ], $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 CompletionItem( + 'staticTestProperty', + CompletionItemKind::PROPERTY, + '\TestClass[]', + 'Lorem excepteur officia sit anim velit veniam enim.' + ) + ], $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 CompletionItem( + 'staticTestMethod', + CompletionItemKind::METHOD, + 'mixed', // Method return type + 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' + ) + ], $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 CompletionItem( + 'TEST_CLASS_CONST', + CompletionItemKind::VARIABLE, + 'int', + 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' + ) + ], $items); + } + public function testFullyQualifiedClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); From d66cc763bc5f6c8139bb0321107a61d8aeb5e0ba Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 21:24:39 +0100 Subject: [PATCH 10/25] Properly filter completion on empty property --- fixtures/completion/property.php | 2 +- src/CompletionProvider.php | 34 +++++++++++++++++--- src/DefinitionResolver.php | 25 ++++++++++++++ tests/Server/TextDocument/CompletionTest.php | 28 ++++++++++++++-- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/fixtures/completion/property.php b/fixtures/completion/property.php index 677bcbbc..17ae95d1 100644 --- a/fixtures/completion/property.php +++ b/fixtures/completion/property.php @@ -1,4 +1,4 @@ t +$obj-> diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index a8d40205..f13e3438 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -56,12 +56,36 @@ public function provideCompletion(PhpDocument $document, Position $position): ar || $node instanceof Node\Expr\StaticPropertyFetch || $node instanceof Node\Expr\ClassConstFetch ) { - /** The FQN to be completed */ - $prefix = $this->definitionResolver->resolveReferenceNodeToFqn($node) ?? ''; - $prefixLen = strlen($prefix); + $nodeToResolve = $node; + 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) { + $nodeToResolve = $node->var; + } else { + $nodeToResolve = $node->class; + } + } + $prefixes = DefinitionResolver::getFqnsFromType( + $this->definitionResolver->resolveExpressionNodeToType($nodeToResolve) + ); + if (!is_string($node->name)) { + // 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 .= '::$'; + } + } + } foreach ($this->project->getDefinitions() as $fqn => $def) { - if (substr($fqn, 0, $prefixLen) === $prefix && !$def->isGlobal) { - $items[] = CompletionItem::fromDefinition($def); + foreach ($prefixes as $prefix) { + if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { + $items[] = CompletionItem::fromDefinition($def); + } } } } else if ( diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index e9e5eacf..bda1e8be 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -146,6 +146,31 @@ public function resolveReferenceNodeToDefinition(Node $node) 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 * Returns null if the FQN could not be resolved or the reference node references a variable diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 3b57c826..36ea2651 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -30,9 +30,9 @@ public function setUp() $this->textDocument = new Server\TextDocument($this->project, $client); } - public function testForPropertiesAndMethods() + public function testPropertyAndMethodWithPrefix() { - $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); $this->project->openDocument($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), @@ -54,6 +54,30 @@ public function testForPropertiesAndMethods() ], $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 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.' + ) + ], $items); + } + public function testVariable() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); From 8f30819a175629ba59d9ccb7395e77e465497e39 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 22:48:11 +0100 Subject: [PATCH 11/25] Fix existing tests --- src/CompletionProvider.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index f13e3438..d09afe51 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -56,19 +56,16 @@ public function provideCompletion(PhpDocument $document, Position $position): ar || $node instanceof Node\Expr\StaticPropertyFetch || $node instanceof Node\Expr\ClassConstFetch ) { - $nodeToResolve = $node; 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) { - $nodeToResolve = $node->var; + // For instances, resolve the variable type + $prefixes = DefinitionResolver::getFqnsFromType( + $this->definitionResolver->resolveExpressionNodeToType($node->var) + ); } else { - $nodeToResolve = $node->class; - } + $prefixes = [is_string($node->class) ? $node->class : '']; } - $prefixes = DefinitionResolver::getFqnsFromType( - $this->definitionResolver->resolveExpressionNodeToType($nodeToResolve) - ); - if (!is_string($node->name)) { // 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) { @@ -80,7 +77,10 @@ public function provideCompletion(PhpDocument $document, Position $position): ar $prefix .= '::$'; } } + } else { + $prefixes = [$this->definitionResolver->resolveReferenceNodeToFqn($node)]; } + foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { From 635e6b3a8d44495053221e3ce0147beb89b0ea57 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 22 Nov 2016 22:57:07 +0100 Subject: [PATCH 12/25] Add support for static access without prefix --- composer.json | 2 +- src/CompletionProvider.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index b29008c4..5f5bf57b 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "bin": ["bin/php-language-server.php"], "require": { "php": ">=7.0", - "nikic/php-parser": "dev-master#c5cdd5ad73ac20d855b84fa6d0f1f22ebff2e302", + "nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^5.0", "felixfbecker/advanced-json-rpc": "^2.0", diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index d09afe51..ed9e382b 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -45,6 +45,10 @@ public function provideCompletion(PhpDocument $document, Position $position): ar { $node = $document->getNodeAtPosition($position); + if ($node instanceof Node\Expr\Error) { + $node = $node->getAttribute('parentNode'); + } + /** @var CompletionItem[] */ $items = []; @@ -64,8 +68,8 @@ public function provideCompletion(PhpDocument $document, Position $position): ar $this->definitionResolver->resolveExpressionNodeToType($node->var) ); } else { - $prefixes = [is_string($node->class) ? $node->class : '']; - } + $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) { From e376ef156958013046e134b22550d6b1d937aec8 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 23 Nov 2016 00:04:36 +0100 Subject: [PATCH 13/25] Fix testFullyQualifiedClass --- tests/Server/TextDocument/CompletionTest.php | 35 +++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 36ea2651..e5ab2595 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -173,6 +173,36 @@ public function testStaticPropertyWithPrefix() ], $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 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.' + ), + new CompletionItem( + 'staticTestMethod', + CompletionItemKind::METHOD, + 'mixed', // Method return type + 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' + ) + ], $items); + } + public function testStaticMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); @@ -222,7 +252,10 @@ public function testFullyQualifiedClass() 'TestClass', CompletionItemKind::CLASS_, null, - 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' + 'Pariatur ut laborum tempor voluptate consequat ea deserunt.', + null, + null, + 'TestClass' ) ], $items); } From c4323c66aa338aed14fe9955483b5c61d967ffc5 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 23 Nov 2016 00:10:09 +0100 Subject: [PATCH 14/25] Add missing fixtures --- fixtures/completion/property_with_prefix.php | 4 ++++ fixtures/completion/static.php | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 fixtures/completion/property_with_prefix.php create mode 100644 fixtures/completion/static.php diff --git a/fixtures/completion/property_with_prefix.php b/fixtures/completion/property_with_prefix.php new file mode 100644 index 00000000..677bcbbc --- /dev/null +++ b/fixtures/completion/property_with_prefix.php @@ -0,0 +1,4 @@ +t diff --git a/fixtures/completion/static.php b/fixtures/completion/static.php new file mode 100644 index 00000000..f975d7ca --- /dev/null +++ b/fixtures/completion/static.php @@ -0,0 +1,3 @@ + Date: Wed, 23 Nov 2016 00:13:30 +0100 Subject: [PATCH 15/25] Correct file number in init test --- tests/LanguageServerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 6892f8fa..b04da917 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -64,7 +64,7 @@ public function testIndexingWithDirectFileAccess() if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); - } else if (strpos($msg->body->params->message, 'All 12 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) { $promise->fulfill(); } } @@ -109,7 +109,7 @@ public function testIndexingWithFilesAndContentRequests() if ($promise->state === Promise::PENDING) { $promise->reject(new Exception($msg->body->params->message)); } - } else if (strpos($msg->body->params->message, 'All 12 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) { // Indexing finished $promise->fulfill(); } From 6adb3f48e1412577fa2002ba543ac58c1855a135 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 23 Nov 2016 00:13:44 +0100 Subject: [PATCH 16/25] Only insert backslash if not typed yet --- src/CompletionProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index ed9e382b..d2f4d0ad 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -155,7 +155,7 @@ public function provideCompletion(PhpDocument $document, Position $position): ar 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) { + } else if ($namespace && !($prefix && $isFullyQualified)) { // Insert the global FQN with trailing backslash $item->insertText = '\\' . $fqn; } else { From f6a7ce1a8b24a6b3768259c2075397e4764bd8b4 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 24 Nov 2016 12:27:55 +0100 Subject: [PATCH 17/25] Completion for keywords and bug fixes --- fixtures/completion/html.php | 0 fixtures/completion/html_with_prefix.php | 1 + fixtures/completion/keywords.php | 3 + src/CompletionProvider.php | 103 +++++++++++++++++-- src/Definition.php | 7 ++ src/DefinitionResolver.php | 33 ++++-- src/PhpDocument.php | 16 +++ src/Protocol/CompletionItem.php | 3 + src/Protocol/Position.php | 12 +++ src/utils.php | 22 ++++ tests/LanguageServerTest.php | 4 +- tests/Server/TextDocument/CompletionTest.php | 80 +++++++++++++- tests/Utils/StripStringOverlapTest.php | 45 ++++++++ 13 files changed, 306 insertions(+), 23 deletions(-) create mode 100644 fixtures/completion/html.php create mode 100644 fixtures/completion/html_with_prefix.php create mode 100644 fixtures/completion/keywords.php create mode 100644 tests/Utils/StripStringOverlapTest.php diff --git a/fixtures/completion/html.php b/fixtures/completion/html.php new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/completion/html_with_prefix.php b/fixtures/completion/html_with_prefix.php new file mode 100644 index 00000000..93184183 --- /dev/null +++ b/fixtures/completion/html_with_prefix.php @@ -0,0 +1 @@ +< diff --git a/fixtures/completion/keywords.php b/fixtures/completion/keywords.php new file mode 100644 index 00000000..76fc7cb7 --- /dev/null +++ b/fixtures/completion/keywords.php @@ -0,0 +1,3 @@ +', + '__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 */ @@ -38,12 +111,12 @@ public function __construct(DefinitionResolver $definitionResolver, Project $pro * Returns suggestions for a specific cursor position in a document * * @param PhpDocument $document The opened document - * @param Position $position The cursor position + * @param Position $pos The cursor position * @return CompletionItem[] */ - public function provideCompletion(PhpDocument $document, Position $position): array + public function provideCompletion(PhpDocument $document, Position $pos): array { - $node = $document->getNodeAtPosition($position); + $node = $document->getNodeAtPosition($pos); if ($node instanceof Node\Expr\Error) { $node = $node->getAttribute('parentNode'); @@ -93,11 +166,12 @@ public function provideCompletion(PhpDocument $document, Position $position): ar } } } else if ( - // A ConstFetch means any static reference, like a class, interface, etc. + // 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 = null; + $prefix = ''; + $prefixLen = 0; if ($node instanceof Node\Name) { $isFullyQualified = $node->isFullyQualified(); $prefix = (string)$node; @@ -115,7 +189,7 @@ public function provideCompletion(PhpDocument $document, Position $position): ar 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, $use->name); + $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name); $aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); } } else { @@ -165,6 +239,16 @@ public function provideCompletion(PhpDocument $document, Position $position): ar $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 . ' '; + $items[] = $item; + } + } + } } else if ( $node instanceof Node\Expr\Variable || ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable) @@ -180,6 +264,13 @@ public function provideCompletion(PhpDocument $document, Position $position): ar $item->detail = (string)$this->definitionResolver->getTypeFromNode($var); $items[] = $item; } + } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) { + $item = new CompletionItem('textEdit = new TextEdit( + new Range($pos, $pos), + stripStringOverlap($document->getRange(new Range(new Position(0, 0), $pos)), '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); @@ -664,28 +668,35 @@ public function getTypeFromNode(Node $node) if ($node instanceof Node\Param) { // Parameters $docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock'); - if ( - $docBlock !== null - && !empty($paramTags = $docBlock->getTagsByName('param')) - && $paramTags[0]->getType() !== null - ) { + if ($docBlock !== null) { // 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) { // Use PHP7 return type hint if (is_string($node->type)) { // Resolve a string like "bool" to a type object $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) { - $defaultType = $this->resolveExpressionNodeToType($node->default); + } + if ($node->default !== null) { + $defaultType = $this->resolveExpressionNodeToType($node->default); + if (isset($type) && !is_a($type, get_class($defaultType))) { $type = new Types\Compound([$type, $defaultType]); + } else { + $type = $defaultType; } } - // Unknown parameter type - return new Types\Mixed; + return $type ?? new Types\Mixed; } if ($node instanceof Node\FunctionLike) { // Functions/methods diff --git a/src/PhpDocument.php b/src/PhpDocument.php index d37ec0a0..cf16495a 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -300,6 +300,22 @@ public function getNodeAtPosition(Position $position) 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 * diff --git a/src/Protocol/CompletionItem.php b/src/Protocol/CompletionItem.php index a764175e..64bc69da 100644 --- a/src/Protocol/CompletionItem.php +++ b/src/Protocol/CompletionItem.php @@ -156,6 +156,9 @@ public static function fromDefinition(Definition $def): self if ($def->documentation) { $item->documentation = $def->documentation; } + if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) { + $item->insertText = '$' . $def->symbolInformation->name; + } return $item; } } diff --git a/src/Protocol/Position.php b/src/Protocol/Position.php index 01cff0b6..1012fdee 100644 --- a/src/Protocol/Position.php +++ b/src/Protocol/Position.php @@ -49,4 +49,16 @@ public function compare(Position $position): int 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); + return array_sum(array_map('strlen', array_slice($lines, 0, $this->line))) + $this->character; + } } diff --git a/src/utils.php b/src/utils.php index 172ccac3..ed7a4198 100644 --- a/src/utils.php +++ b/src/utils.php @@ -95,3 +95,25 @@ function getClosestNode(Node $node, string $type) } } } + +/** + * Returns the part of $b that is not overlapped by $a + * Example: + * + * stripStringOverlap('whateverbody->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); - } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) { $promise->fulfill(); } } @@ -109,7 +109,7 @@ public function testIndexingWithFilesAndContentRequests() if ($promise->state === Promise::PENDING) { $promise->reject(new Exception($msg->body->params->message)); } - } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) { // Indexing finished $promise->fulfill(); } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index e5ab2595..a7afb0d7 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -5,8 +5,16 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ClientCapabilities, CompletionItem, CompletionItemKind}; +use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; +use LanguageServer\Protocol\{ + TextDocumentIdentifier, + TextEdit, + Range, + Position, + ClientCapabilities, + CompletionItem, + CompletionItemKind +}; use function LanguageServer\pathToUri; class CompletionTest extends TestCase @@ -168,7 +176,10 @@ public function testStaticPropertyWithPrefix() 'staticTestProperty', CompletionItemKind::PROPERTY, '\TestClass[]', - 'Lorem excepteur officia sit anim velit veniam enim.' + 'Lorem excepteur officia sit anim velit veniam enim.', + null, + null, + '$staticTestProperty' ) ], $items); } @@ -192,7 +203,10 @@ public function testStaticWithoutPrefix() 'staticTestProperty', CompletionItemKind::PROPERTY, '\TestClass[]', - 'Lorem excepteur officia sit anim velit veniam enim.' + 'Lorem excepteur officia sit anim velit veniam enim.', + null, + null, + '$staticTestProperty' ), new CompletionItem( 'staticTestMethod', @@ -259,4 +273,62 @@ public function testFullyQualifiedClass() ) ], $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 CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '), + new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ') + ], $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 CompletionItem( + 'project->openDocument($completionUri, file_get_contents($completionUri)); + $items = $this->textDocument->completion( + new TextDocumentIdentifier($completionUri), + new Position(0, 1) + )->wait(); + $this->assertEquals([ + new CompletionItem( + 'assertEquals('assertEquals('?php', stripStringOverlap('bla<', 'assertEquals('php', stripStringOverlap('blaassertEquals('', stripStringOverlap('blaassertEquals('assertEquals('', stripStringOverlap('bla', '')); + } + + public function testBothEmpty() + { + $this->assertEquals('', stripStringOverlap('', '')); + } +} From 6fb21817e4bef85dfb9a6e6f32d36d1507f33794 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 24 Nov 2016 13:26:22 +0100 Subject: [PATCH 18/25] Correct variable insertion --- src/CompletionProvider.php | 12 ++++--- src/Protocol/Position.php | 3 +- tests/Server/TextDocument/CompletionTest.php | 35 +++++++++++++++++--- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index f443136e..f0c9c3e6 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -110,13 +110,13 @@ public function __construct(DefinitionResolver $definitionResolver, Project $pro /** * Returns suggestions for a specific cursor position in a document * - * @param PhpDocument $document The opened document + * @param PhpDocument $doc The opened document * @param Position $pos The cursor position * @return CompletionItem[] */ - public function provideCompletion(PhpDocument $document, Position $pos): array + public function provideCompletion(PhpDocument $doc, Position $pos): array { - $node = $document->getNodeAtPosition($pos); + $node = $doc->getNodeAtPosition($pos); if ($node instanceof Node\Expr\Error) { $node = $node->getAttribute('parentNode'); @@ -262,13 +262,17 @@ public function provideCompletion(PhpDocument $document, Position $pos): array $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) + ); $items[] = $item; } } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) { $item = new CompletionItem('textEdit = new TextEdit( new Range($pos, $pos), - stripStringOverlap($document->getRange(new Range(new Position(0, 0), $pos)), 'getRange(new Range(new Position(0, 0), $pos)), 'line))) + $this->character; + $slice = array_slice($lines, 0, $this->line); + return array_sum(array_map('strlen', $slice)) + count($slice) + $this->character; } } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index a7afb0d7..f2e80289 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -95,8 +95,26 @@ public function testVariable() new Position(8, 5) )->wait(); $this->assertEquals([ - new CompletionItem('$var', CompletionItemKind::VARIABLE, 'int'), - new CompletionItem('$param', CompletionItemKind::VARIABLE, 'string|null', 'A parameter') + 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') + ) ], $items); } @@ -106,10 +124,19 @@ public function testVariableWithPrefix() $this->project->openDocument($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), - new Position(8, 5) + new Position(8, 6) )->wait(); $this->assertEquals([ - new CompletionItem('$param', CompletionItemKind::VARIABLE, 'string|null', 'A parameter') + 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') + ) ], $items); } From 5f085a3d8ab3191262ba1619670ec3d4a6e24af7 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 24 Nov 2016 23:36:45 +0100 Subject: [PATCH 19/25] Support completion for namespaces --- fixtures/completion/namespace.php | 5 +++++ src/CompletionProvider.php | 8 +++++-- src/Definition.php | 1 + src/DefinitionResolver.php | 4 ++++ tests/LanguageServerTest.php | 4 ++-- tests/NodeVisitor/DefinitionCollectorTest.php | 4 +++- tests/Server/ServerTestCase.php | 14 ++++++------- tests/Server/TextDocument/CompletionTest.php | 21 +++++++++++++++++++ .../TextDocument/DocumentSymbolTest.php | 1 + tests/Server/Workspace/SymbolTest.php | 18 ++++++++++++++-- 10 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 fixtures/completion/namespace.php diff --git a/fixtures/completion/namespace.php b/fixtures/completion/namespace.php new file mode 100644 index 00000000..25742e0d --- /dev/null +++ b/fixtures/completion/namespace.php @@ -0,0 +1,5 @@ +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_ ); @@ -189,6 +190,7 @@ public function resolveReferenceNodeToFqn(Node $node) if ( $node instanceof Node\Name && ( $parent instanceof Node\Stmt\ClassLike + || $parent instanceof Node\Namespace_ || $parent instanceof Node\Param || $parent instanceof Node\FunctionLike || $parent instanceof Node\Expr\StaticCall @@ -775,6 +777,8 @@ public static function getDefinedFqn(Node $node) if ($node instanceof Node\Stmt\ClassLike && isset($node->name)) { // Class, interface or trait declaration return (string)$node->namespacedName; + } else if ($node instanceof Node\Stmt\Namespace_) { + return (string)$node->name; } else if ($node instanceof Node\Stmt\Function_) { // Function: use functionName() as the name return (string)$node->namespacedName . '()'; diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 8efac8d8..4a35fe6f 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -64,7 +64,7 @@ public function testIndexingWithDirectFileAccess() if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); - } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) { $promise->fulfill(); } } @@ -109,7 +109,7 @@ public function testIndexingWithFilesAndContentRequests() if ($promise->state === Promise::PENDING) { $promise->reject(new Exception($msg->body->params->message)); } - } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) { + } else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) { // Indexing finished $promise->fulfill(); } diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 6768c94c..74e0d5c9 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -30,6 +30,7 @@ public function testCollectsSymbols() $traverser->traverse($stmts); $defNodes = $definitionCollector->nodes; $this->assertEquals([ + 'TestNamespace', 'TestNamespace\\TEST_CONST', 'TestNamespace\\TestClass', 'TestNamespace\\TestClass::TEST_CLASS_CONST', @@ -68,7 +69,8 @@ public function testDoesNotCollectReferences() $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); $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()']); } } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 1a608ca4..23d1763c 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -54,13 +54,11 @@ public function setUp() $referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php')); - Promise\all([ - $this->project->loadDocument($symbolsUri), - $this->project->loadDocument($referencesUri), - $this->project->loadDocument($globalSymbolsUri), - $this->project->loadDocument($globalReferencesUri), - $this->project->loadDocument($useUri) - ])->wait(); + $this->project->loadDocument($symbolsUri)->wait(); + $this->project->loadDocument($referencesUri)->wait(); + $this->project->loadDocument($globalSymbolsUri)->wait(); + $this->project->loadDocument($globalReferencesUri)->wait(); + $this->project->loadDocument($useUri)->wait(); // @codingStandardsIgnoreStart $this->definitionLocations = [ @@ -79,6 +77,8 @@ public function setUp() 'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))), // 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\\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))), diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index f2e80289..c76c7fd9 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -358,4 +358,25 @@ public function testHtmlWithPrefix() ) ], $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 CompletionItem( + 'SomeNamespace', + CompletionItemKind::MODULE, + null, + null, + null, + null, + 'SomeNamespace' + ) + ], $items); + } } diff --git a/tests/Server/TextDocument/DocumentSymbolTest.php b/tests/Server/TextDocument/DocumentSymbolTest.php index 4f09e20d..b9c937ef 100644 --- a/tests/Server/TextDocument/DocumentSymbolTest.php +++ b/tests/Server/TextDocument/DocumentSymbolTest.php @@ -18,6 +18,7 @@ public function test() $result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait(); // @codingStandardsIgnoreStart $this->assertEquals([ + new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('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('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), diff --git a/tests/Server/Workspace/SymbolTest.php b/tests/Server/Workspace/SymbolTest.php index 70869426..33b4cf1d 100644 --- a/tests/Server/Workspace/SymbolTest.php +++ b/tests/Server/Workspace/SymbolTest.php @@ -6,7 +6,17 @@ use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; 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 function LanguageServer\pathToUri; @@ -16,8 +26,10 @@ public function testEmptyQueryReturnsAllSymbols() { // Request symbols $result = $this->workspace->symbol(''); + $referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php')); // @codingStandardsIgnoreStart $this->assertEquals([ + new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''), // Namespaced new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), @@ -41,7 +53,9 @@ public function testEmptyQueryReturnsAllSymbols() new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestTrait'), ''), new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestInterface'), ''), 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); // @codingStandardsIgnoreEnd } From 38383fcaf0f33bff84836f004e5a317b40fa2cf3 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Mon, 28 Nov 2016 18:47:56 +0100 Subject: [PATCH 20/25] Use CompletionList --- src/CompletionProvider.php | 22 +++---- src/Protocol/CompletionList.php | 10 ++++ tests/Server/TextDocument/CompletionTest.php | 61 ++++++++++---------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index e8dcdbc3..4f288116 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -10,6 +10,7 @@ Range, Position, SymbolKind, + CompletionList, CompletionItem, CompletionItemKind }; @@ -112,9 +113,9 @@ public function __construct(DefinitionResolver $definitionResolver, Project $pro * * @param PhpDocument $doc The opened document * @param Position $pos The cursor position - * @return CompletionItem[] + * @return CompletionList */ - public function provideCompletion(PhpDocument $doc, Position $pos): array + public function provideCompletion(PhpDocument $doc, Position $pos): CompletionList { $node = $doc->getNodeAtPosition($pos); @@ -122,8 +123,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array $node = $node->getAttribute('parentNode'); } - /** @var CompletionItem[] */ - $items = []; + $list = new CompletionList; // A non-free node means we do NOT suggest global symbols if ( @@ -161,7 +161,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array foreach ($this->project->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { - $items[] = CompletionItem::fromDefinition($def); + $list->items[] = CompletionItem::fromDefinition($def); } } } @@ -204,7 +204,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array // Search the aliases for the typed-in name foreach ($aliasedDefs as $alias => $def) { if (substr($alias, 0, $prefixLen) === $prefix) { - $items[] = CompletionItem::fromDefinition($def); + $list->items[] = CompletionItem::fromDefinition($def); } } } @@ -240,7 +240,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array // Insert the FQN without trailing backlash $item->insertText = $fqn; } - $items[] = $item; + $list->items[] = $item; } } // Suggest keywords @@ -249,7 +249,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array if (substr($keyword, 0, $prefixLen) === $prefix) { $item = new CompletionItem($keyword, CompletionItemKind::KEYWORD); $item->insertText = $keyword . ' '; - $items[] = $item; + $list->items[] = $item; } } } @@ -270,7 +270,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): array new Range($pos, $pos), stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label) ); - $items[] = $item; + $list->items[] = $item; } } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) { $item = new CompletionItem('getRange(new Range(new Position(0, 0), $pos)), 'items[] = $item; } - return $items; + return $list; } /** diff --git a/src/Protocol/CompletionList.php b/src/Protocol/CompletionList.php index d3488309..4d0bd646 100644 --- a/src/Protocol/CompletionList.php +++ b/src/Protocol/CompletionList.php @@ -22,4 +22,14 @@ class CompletionList * @var CompletionItem[] */ public $items; + + /** + * @param CompletionItem[] $items The completion items. + * @param bool $isIncomplete This list it not complete. Further typing should result in recomputing this list. + */ + public function __construct(array $items = [], bool $isIncomplete = false) + { + $this->items = $items; + $this->isIncomplete = $isIncomplete; + } } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index c76c7fd9..cbbbd7cd 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -12,6 +12,7 @@ Range, Position, ClientCapabilities, + CompletionList, CompletionItem, CompletionItemKind }; @@ -46,7 +47,7 @@ public function testPropertyAndMethodWithPrefix() new TextDocumentIdentifier($completionUri), new Position(3, 7) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'testProperty', CompletionItemKind::PROPERTY, @@ -59,7 +60,7 @@ public function testPropertyAndMethodWithPrefix() '\TestClass', // Return type of the method 'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.' ) - ], $items); + ]), $items); } public function testPropertyAndMethodWithoutPrefix() @@ -70,7 +71,7 @@ public function testPropertyAndMethodWithoutPrefix() new TextDocumentIdentifier($completionUri), new Position(3, 6) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'testProperty', CompletionItemKind::PROPERTY, @@ -83,7 +84,7 @@ public function testPropertyAndMethodWithoutPrefix() '\TestClass', // Return type of the method 'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.' ) - ], $items); + ]), $items); } public function testVariable() @@ -94,7 +95,7 @@ public function testVariable() new TextDocumentIdentifier($completionUri), new Position(8, 5) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( '$var', CompletionItemKind::VARIABLE, @@ -115,7 +116,7 @@ public function testVariable() null, new TextEdit(new Range(new Position(8, 5), new Position(8, 5)), 'param') ) - ], $items); + ]), $items); } public function testVariableWithPrefix() @@ -126,7 +127,7 @@ public function testVariableWithPrefix() new TextDocumentIdentifier($completionUri), new Position(8, 6) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( '$param', CompletionItemKind::VARIABLE, @@ -137,7 +138,7 @@ public function testVariableWithPrefix() null, new TextEdit(new Range(new Position(8, 6), new Position(8, 6)), 'aram') ) - ], $items); + ]), $items); } public function testNewInNamespace() @@ -148,7 +149,7 @@ public function testNewInNamespace() new TextDocumentIdentifier($completionUri), new Position(6, 10) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ // Global TestClass definition (inserted as \TestClass) new CompletionItem( 'TestClass', @@ -169,7 +170,7 @@ public function testNewInNamespace() null, 'TestClass' ), - ], $items); + ]), $items); } public function testUsedClass() @@ -180,14 +181,14 @@ public function testUsedClass() new TextDocumentIdentifier($completionUri), new Position(6, 5) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'TestClass', CompletionItemKind::CLASS_, 'TestNamespace', 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' ) - ], $items); + ]), $items); } public function testStaticPropertyWithPrefix() @@ -198,7 +199,7 @@ public function testStaticPropertyWithPrefix() new TextDocumentIdentifier($completionUri), new Position(2, 14) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'staticTestProperty', CompletionItemKind::PROPERTY, @@ -208,7 +209,7 @@ public function testStaticPropertyWithPrefix() null, '$staticTestProperty' ) - ], $items); + ]), $items); } public function testStaticWithoutPrefix() @@ -219,7 +220,7 @@ public function testStaticWithoutPrefix() new TextDocumentIdentifier($completionUri), new Position(2, 11) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'TEST_CLASS_CONST', CompletionItemKind::VARIABLE, @@ -241,7 +242,7 @@ public function testStaticWithoutPrefix() 'mixed', // Method return type 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) - ], $items); + ]), $items); } public function testStaticMethodWithPrefix() @@ -252,14 +253,14 @@ public function testStaticMethodWithPrefix() new TextDocumentIdentifier($completionUri), new Position(2, 13) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'staticTestMethod', CompletionItemKind::METHOD, 'mixed', // Method return type 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) - ], $items); + ]), $items); } public function testClassConstWithPrefix() @@ -270,14 +271,14 @@ public function testClassConstWithPrefix() new TextDocumentIdentifier($completionUri), new Position(2, 13) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'TEST_CLASS_CONST', CompletionItemKind::VARIABLE, 'int', 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' ) - ], $items); + ]), $items); } public function testFullyQualifiedClass() @@ -288,7 +289,7 @@ public function testFullyQualifiedClass() new TextDocumentIdentifier($completionUri), new Position(6, 6) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'TestClass', CompletionItemKind::CLASS_, @@ -298,7 +299,7 @@ public function testFullyQualifiedClass() null, 'TestClass' ) - ], $items); + ]), $items); } public function testKeywords() @@ -309,10 +310,10 @@ public function testKeywords() new TextDocumentIdentifier($completionUri), new Position(2, 1) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '), new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ') - ], $items); + ]), $items); } public function testHtmlWithoutPrefix() @@ -323,7 +324,7 @@ public function testHtmlWithoutPrefix() new TextDocumentIdentifier($completionUri), new Position(0, 0) )->wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'wait(); - $this->assertEquals([ + $this->assertEquals(new CompletionList([ new CompletionItem( 'SomeNamespace', CompletionItemKind::MODULE, @@ -377,6 +378,6 @@ public function testNamespace() null, 'SomeNamespace' ) - ], $items); + ]), $items); } } From 04a517c674fd27800877fa6098fcd4567cf43066 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 29 Nov 2016 22:16:57 +0100 Subject: [PATCH 21/25] Always set isIncomplete to true --- src/CompletionProvider.php | 21 ++++++++++++++ tests/Server/TextDocument/CompletionTest.php | 30 ++++++++++---------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 4f288116..e97fd389 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -124,6 +124,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi } $list = new CompletionList; + $list->isIncomplete = true; // A non-free node means we do NOT suggest global symbols if ( @@ -380,4 +381,24 @@ private function findVariableDefinitionsInNode(Node $node, string $namePrefix = } return $vars; } + + private static function createTextEdit(PhpDocument $doc, Position $pos, string $insert): string + { + $content = $doc->getContent(); + $offset = $pos->toOffset($content); + $contentLen = strlen($content); + for ($i = $offset; $i <= $contentLen; $i++) { + if (substr($content, $offset, $offset + $i) === substr($content, $offset - $i)) { + $insert = substr($offset, $i); + break; + } + } + for ($i = $offset; $i <= $contentLen; $i++) { + if (substr($content, $offset, $i) === substr($content, $headLen - $i)) { + $insert = substr($insert, $i); + break; + } + } + return new TextEdit(new Range($pos, $pos), $insert); + } } diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index cbbbd7cd..7b5dd0ac 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -60,7 +60,7 @@ public function testPropertyAndMethodWithPrefix() '\TestClass', // Return type of the method 'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.' ) - ]), $items); + ], true), $items); } public function testPropertyAndMethodWithoutPrefix() @@ -84,7 +84,7 @@ public function testPropertyAndMethodWithoutPrefix() '\TestClass', // Return type of the method 'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.' ) - ]), $items); + ], true), $items); } public function testVariable() @@ -116,7 +116,7 @@ public function testVariable() null, new TextEdit(new Range(new Position(8, 5), new Position(8, 5)), 'param') ) - ]), $items); + ], true), $items); } public function testVariableWithPrefix() @@ -138,7 +138,7 @@ public function testVariableWithPrefix() null, new TextEdit(new Range(new Position(8, 6), new Position(8, 6)), 'aram') ) - ]), $items); + ], true), $items); } public function testNewInNamespace() @@ -170,7 +170,7 @@ public function testNewInNamespace() null, 'TestClass' ), - ]), $items); + ], true), $items); } public function testUsedClass() @@ -188,7 +188,7 @@ public function testUsedClass() 'TestNamespace', 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' ) - ]), $items); + ], true), $items); } public function testStaticPropertyWithPrefix() @@ -209,7 +209,7 @@ public function testStaticPropertyWithPrefix() null, '$staticTestProperty' ) - ]), $items); + ], true), $items); } public function testStaticWithoutPrefix() @@ -242,7 +242,7 @@ public function testStaticWithoutPrefix() 'mixed', // Method return type 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) - ]), $items); + ], true), $items); } public function testStaticMethodWithPrefix() @@ -260,7 +260,7 @@ public function testStaticMethodWithPrefix() 'mixed', // Method return type 'Do magna consequat veniam minim proident eiusmod incididunt aute proident.' ) - ]), $items); + ], true), $items); } public function testClassConstWithPrefix() @@ -278,7 +278,7 @@ public function testClassConstWithPrefix() 'int', 'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.' ) - ]), $items); + ], true), $items); } public function testFullyQualifiedClass() @@ -299,7 +299,7 @@ public function testFullyQualifiedClass() null, 'TestClass' ) - ]), $items); + ], true), $items); } public function testKeywords() @@ -313,7 +313,7 @@ public function testKeywords() $this->assertEquals(new CompletionList([ new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '), new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ') - ]), $items); + ], true), $items); } public function testHtmlWithoutPrefix() @@ -335,7 +335,7 @@ public function testHtmlWithoutPrefix() null, new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ' Date: Tue, 29 Nov 2016 22:27:49 +0100 Subject: [PATCH 22/25] Update PHPCodeSniffer --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5f5bf57b..224d3d01 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^5.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", "webmozart/path-util": "^2.3", "webmozart/glob": "^4.1", From c91a5738cf497ba064fbb7fd5f38680421b46733 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 30 Nov 2016 00:11:23 +0100 Subject: [PATCH 23/25] Remove unused method --- src/CompletionProvider.php | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index e97fd389..39bf76e3 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -275,10 +275,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi } } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) { $item = new CompletionItem('textEdit = new TextEdit( - new Range($pos, $pos), - stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), 'textEdit = $this->createTextEdit($doc, $pos, 'items[] = $item; } @@ -381,24 +378,4 @@ private function findVariableDefinitionsInNode(Node $node, string $namePrefix = } return $vars; } - - private static function createTextEdit(PhpDocument $doc, Position $pos, string $insert): string - { - $content = $doc->getContent(); - $offset = $pos->toOffset($content); - $contentLen = strlen($content); - for ($i = $offset; $i <= $contentLen; $i++) { - if (substr($content, $offset, $offset + $i) === substr($content, $offset - $i)) { - $insert = substr($offset, $i); - break; - } - } - for ($i = $offset; $i <= $contentLen; $i++) { - if (substr($content, $offset, $i) === substr($content, $headLen - $i)) { - $insert = substr($insert, $i); - break; - } - } - return new TextEdit(new Range($pos, $pos), $insert); - } } From 03faea59113f3ab206b02d65a7d8c4ec9fbe2965 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 30 Nov 2016 00:15:20 +0100 Subject: [PATCH 24/25] And the call --- src/CompletionProvider.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 39bf76e3..024134d6 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -275,7 +275,10 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi } } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) { $item = new CompletionItem('textEdit = $this->createTextEdit($doc, $pos, 'textEdit = new TextEdit( + new Range($pos, $pos), + stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), 'items[] = $item; } From cc8365d425bc1049222213ffe88c85823c1dea10 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Wed, 30 Nov 2016 11:17:38 +0100 Subject: [PATCH 25/25] Handle case where FQN could not be resolved --- src/CompletionProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 024134d6..261bc1d3 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -156,7 +156,8 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi } } } else { - $prefixes = [$this->definitionResolver->resolveReferenceNodeToFqn($node)]; + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + $prefixes = $fqn !== null ? [$fqn] : []; } foreach ($this->project->getDefinitions() as $fqn => $def) {