Add support for method/property completion
parent
429114ff97
commit
44d26ba1aa
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$obj = new TestClass;
|
||||||
|
$obj->t
|
|
@ -327,7 +327,7 @@ class DefinitionResolver
|
||||||
* @param \PhpParser\Node\Expr $expr
|
* @param \PhpParser\Node\Expr $expr
|
||||||
* @return \phpDocumentor\Type
|
* @return \phpDocumentor\Type
|
||||||
*/
|
*/
|
||||||
private function resolveExpressionNodeToType(Node\Expr $expr): Type
|
public function resolveExpressionNodeToType(Node\Expr $expr): Type
|
||||||
{
|
{
|
||||||
if ($expr instanceof Node\Expr\Variable) {
|
if ($expr instanceof Node\Expr\Variable) {
|
||||||
if ($expr->name === 'this') {
|
if ($expr->name === 'this') {
|
||||||
|
|
|
@ -11,7 +11,8 @@ use LanguageServer\Protocol\{
|
||||||
MessageType,
|
MessageType,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
SymbolInformation,
|
SymbolInformation,
|
||||||
TextDocumentIdentifier
|
TextDocumentIdentifier,
|
||||||
|
CompletionOptions
|
||||||
};
|
};
|
||||||
use AdvancedJsonRpc;
|
use AdvancedJsonRpc;
|
||||||
use Sabre\Event\{Loop, Promise};
|
use Sabre\Event\{Loop, Promise};
|
||||||
|
@ -140,6 +141,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->referencesProvider = true;
|
$serverCapabilities->referencesProvider = true;
|
||||||
// Support "Hover"
|
// Support "Hover"
|
||||||
$serverCapabilities->hoverProvider = true;
|
$serverCapabilities->hoverProvider = true;
|
||||||
|
// Support "Completion"
|
||||||
|
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||||
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||||
|
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,24 @@ class CompletionItem
|
||||||
*/
|
*/
|
||||||
public $textEdit;
|
public $textEdit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional array of additional text edits that are applied when
|
||||||
|
* selecting this completion. Edits must not overlap with the main edit
|
||||||
|
* nor with themselves.
|
||||||
|
*
|
||||||
|
* @var TextEdit[]|null
|
||||||
|
*/
|
||||||
|
public $additionalTextEdits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional command that is executed *after* inserting this completion. *Note* that
|
||||||
|
* additional modifications to the current document should be described with the
|
||||||
|
* additionalTextEdits-property.
|
||||||
|
*
|
||||||
|
* @var Command|null
|
||||||
|
*/
|
||||||
|
public $command;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An data entry field that is preserved on a completion item between
|
* An data entry field that is preserved on a completion item between
|
||||||
* a completion and a completion resolve request.
|
* a completion and a completion resolve request.
|
||||||
|
@ -76,4 +94,43 @@ class CompletionItem
|
||||||
* @var mixed
|
* @var mixed
|
||||||
*/
|
*/
|
||||||
public $data;
|
public $data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $label
|
||||||
|
* @param int|null $kind
|
||||||
|
* @param string|null $detail
|
||||||
|
* @param string|null $documentation
|
||||||
|
* @param string|null $sortText
|
||||||
|
* @param string|null $filterText
|
||||||
|
* @param string|null $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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ abstract class CompletionItemKind
|
||||||
const CONSTRUCTOR = 4;
|
const CONSTRUCTOR = 4;
|
||||||
const FIELD = 5;
|
const FIELD = 5;
|
||||||
const VARIABLE = 6;
|
const VARIABLE = 6;
|
||||||
const _CLASS = 7;
|
const CLASS_ = 7;
|
||||||
const INTERFACE = 8;
|
const INTERFACE = 8;
|
||||||
const MODULE = 9;
|
const MODULE = 9;
|
||||||
const PROPERTY = 10;
|
const PROPERTY = 10;
|
||||||
|
|
|
@ -11,14 +11,14 @@ class CompletionOptions
|
||||||
* The server provides support to resolve additional information for a completion
|
* The server provides support to resolve additional information for a completion
|
||||||
* item.
|
* item.
|
||||||
*
|
*
|
||||||
* @var bool
|
* @var bool|null
|
||||||
*/
|
*/
|
||||||
public $resolveProvider;
|
public $resolveProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The characters that trigger completion automatically.
|
* The characters that trigger completion automatically.
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string[]|null
|
||||||
*/
|
*/
|
||||||
public $triggerCharacters;
|
public $triggerCharacters;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,12 @@ use LanguageServer\Protocol\{
|
||||||
SymbolInformation,
|
SymbolInformation,
|
||||||
ReferenceContext,
|
ReferenceContext,
|
||||||
Hover,
|
Hover,
|
||||||
MarkedString
|
MarkedString,
|
||||||
|
SymbolKind,
|
||||||
|
CompletionItem,
|
||||||
|
CompletionItemKind
|
||||||
};
|
};
|
||||||
|
use phpDocumentor\Reflection\Types;
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use function Sabre\Event\coroutine;
|
use function Sabre\Event\coroutine;
|
||||||
|
|
||||||
|
@ -210,4 +214,64 @@ class TextDocument
|
||||||
return new Hover($contents, $range);
|
return new Hover($contents, $range);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Completion request is sent from the client to the server to compute completion items at a given cursor
|
||||||
|
* position. Completion items are presented in the IntelliSense user interface. If computing full completion items
|
||||||
|
* is expensive, servers can additionally provide a handler for the completion item resolve request
|
||||||
|
* ('completionItem/resolve'). This request is sent when a completion item is selected in the user interface. A
|
||||||
|
* typically use case is for example: the 'textDocument/completion' request doesn't fill in the documentation
|
||||||
|
* property for returned completion items since it is expensive to compute. When the item is selected in the user
|
||||||
|
* interface then a 'completionItem/resolve' request is sent with the selected completion item as a param. The
|
||||||
|
* returned completion item should have the documentation property filled in.
|
||||||
|
*
|
||||||
|
* @param TextDocumentIdentifier The text document
|
||||||
|
* @param Position $position The position
|
||||||
|
* @return Promise <CompletionItem[]|CompletionList>
|
||||||
|
*/
|
||||||
|
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
|
{
|
||||||
|
return coroutine(function () use ($textDocument, $position) {
|
||||||
|
$document = yield $this->project->getOrLoadDocument($textDocument->uri);
|
||||||
|
$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 [];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,10 @@ class LanguageServerTest extends TestCase
|
||||||
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
||||||
'documentSymbolProvider' => true,
|
'documentSymbolProvider' => true,
|
||||||
'hoverProvider' => true,
|
'hoverProvider' => true,
|
||||||
'completionProvider' => null,
|
'completionProvider' => (object)[
|
||||||
|
'resolveProvider' => false,
|
||||||
|
'triggerCharacters' => ['$', '>']
|
||||||
|
],
|
||||||
'signatureHelpProvider' => null,
|
'signatureHelpProvider' => null,
|
||||||
'definitionProvider' => true,
|
'definitionProvider' => true,
|
||||||
'referencesProvider' => true,
|
'referencesProvider' => true,
|
||||||
|
@ -61,7 +64,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||||
if ($msg->body->params->type === MessageType::ERROR) {
|
if ($msg->body->params->type === MessageType::ERROR) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
} else if (strpos($msg->body->params->message, 'All 10 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 11 PHP files parsed') !== false) {
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +109,7 @@ class LanguageServerTest extends TestCase
|
||||||
if ($promise->state === Promise::PENDING) {
|
if ($promise->state === Promise::PENDING) {
|
||||||
$promise->reject(new Exception($msg->body->params->message));
|
$promise->reject(new Exception($msg->body->params->message));
|
||||||
}
|
}
|
||||||
} else if (strpos($msg->body->params->message, 'All 10 PHP files parsed') !== false) {
|
} else if (strpos($msg->body->params->message, 'All 11 PHP files parsed') !== false) {
|
||||||
// Indexing finished
|
// Indexing finished
|
||||||
$promise->fulfill();
|
$promise->fulfill();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue