Skip to content

Commit

Permalink
Add support for method/property completion
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfbecker committed Nov 19, 2016
1 parent 429114f commit 44d26ba
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 9 deletions.
4 changes: 4 additions & 0 deletions fixtures/completion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

$obj = new TestClass;
$obj->t
2 changes: 1 addition & 1 deletion src/DefinitionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
7 changes: 6 additions & 1 deletion src/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
MessageType,
InitializeResult,
SymbolInformation,
TextDocumentIdentifier
TextDocumentIdentifier,
CompletionOptions
};
use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise};
Expand Down Expand Up @@ -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);
}
Expand Down
57 changes: 57 additions & 0 deletions src/Protocol/CompletionItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,68 @@ 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.
*
* @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;
}
}
2 changes: 1 addition & 1 deletion src/Protocol/CompletionItemKind.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/Protocol/CompletionOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
66 changes: 65 additions & 1 deletion src/Server/TextDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 <CompletionItem[]|CompletionList>
*/
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 [];
});
}
}
9 changes: 6 additions & 3 deletions tests/LanguageServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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();
}
Expand Down
55 changes: 55 additions & 0 deletions tests/Server/TextDocument/CompletionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
declare(strict_types = 1);

namespace LanguageServer\Tests\Server\TextDocument;

use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ClientCapabilities, CompletionItem, CompletionItemKind};
use function LanguageServer\pathToUri;

class CompletionTest extends TestCase
{
/**
* @var Server\TextDocument
*/
private $textDocument;

/**
* @var string
*/
private $completionUri;

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);
}

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);
}
}

0 comments on commit 44d26ba

Please sign in to comment.