Skip to content

Signature help #438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
7 changes: 7 additions & 0 deletions fixtures/signature/funcClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

function helpFunc1(int $count = 0)
{
}

helpFunc1()
7 changes: 7 additions & 0 deletions fixtures/signature/funcNotClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

function helpFunc2(int $count = 0)
{
}

helpFunc2(
15 changes: 15 additions & 0 deletions fixtures/signature/methodActiveParam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

class HelpClass5
{
public function method(string $param = "", int $count = 0, bool $test = null)
{
}
public function test()
{
$this->method();
}
}

$a = new HelpClass5;
$a->method("asdf", 123, true);
15 changes: 15 additions & 0 deletions fixtures/signature/methodClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

class HelpClass1
{
public function method(string $param = "")
{
}
public function test()
{
$this->method();
}
}

$a = new HelpClass1;
$a->method();
17 changes: 17 additions & 0 deletions fixtures/signature/methodNotClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

class HelpClass2
{
protected function method(string $param = "")
{
}
public function test()
{
$this->method(1,1);
}
}
$a = new HelpClass2;
$a
->method(
1,
array(),
10 changes: 10 additions & 0 deletions fixtures/signature/staticClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

class HelpClass3
{
public static function method(string $param = "")
{
}
}

HelpClass3::method()
10 changes: 10 additions & 0 deletions fixtures/signature/staticNotClosed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

class HelpClass4
{
public static function method(string $param = "")
{
}
}

HelpClass4::method(1
8 changes: 8 additions & 0 deletions src/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use LanguageServer\Index\ReadableIndex;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation;
use LanguageServer\Protocol\ParameterInformation;
use Exception;
use Generator;

Expand Down Expand Up @@ -98,6 +99,13 @@ class Definition
* @var string
*/
public $documentation;

/**
* Parameters array (for methods and functions), for use in textDocument/signatureHelp
*
* @var ParameterInformation[]
*/
public $parameters;

/**
* Yields the definitons of all ancestor classes (the Definition fqn is yielded as key)
Expand Down
18 changes: 18 additions & 0 deletions src/DefinitionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@

use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\SymbolInformation;
use LanguageServer\Protocol\ParameterInformation;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\Statement\FunctionDeclaration;
use phpDocumentor\Reflection\{
DocBlock, DocBlockFactory, Fqsen, Type, TypeResolver, Types
};
Expand Down Expand Up @@ -232,6 +236,20 @@ public function createDefinitionFromNode(Node $node, string $fqn = null): Defini
$def->documentation = $this->getDocumentationFromNode($node);
}

$def->parameters = [];
if (($node instanceof MethodDeclaration ||
$node instanceof FunctionDeclaration ||
$node instanceof AnonymousFunctionCreationExpression) &&
$node->parameters !== null
) {
foreach ($node->parameters->getElements() as $param) {
$def->parameters[] = new ParameterInformation(
$this->getDeclarationLineFromNode($param),
$this->getDocumentationFromNode($param)
);
}
}

return $def;
}

Expand Down
6 changes: 5 additions & 1 deletion src/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
TextDocumentSyncKind,
Message,
InitializeResult,
CompletionOptions
CompletionOptions,
SignatureHelpOptions
};
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
Expand Down Expand Up @@ -275,6 +276,9 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath =
$serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
// support signature help
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions;
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(',','];
// Support global references
$serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true;
Expand Down
9 changes: 9 additions & 0 deletions src/Protocol/ParameterInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,13 @@ class ParameterInformation
* @var string|null
*/
public $documentation;
/**
* @param string $label The label of this signature. Will be shown in the UI.
* @param string|null $documentation The human-readable doc-comment of this signature.
*/
public function __construct(string $label = null, string $documentation = null)
{
$this->label = $label;
$this->documentation = $documentation;
}
}
11 changes: 11 additions & 0 deletions src/Protocol/SignatureHelp.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ class SignatureHelp
* @var int|null
*/
public $activeParameter;
/**
* @param SignatureInformation[] $signatures The signatures.
* @param int|null $activeSignature The active signature.
* @param int|null $activeParameter The active parameter of the active signature.
*/
public function __construct(array $signatures = [], int $activeSignature = null, int $activeParameter = null)
{
$this->signatures = $signatures;
$this->activeSignature = $activeSignature;
$this->activeParameter = $activeParameter;
}
}
12 changes: 12 additions & 0 deletions src/Protocol/SignatureInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ class SignatureInformation
* @var ParameterInformation[]|null
*/
public $parameters;

/**
* @param string $label The label of this signature. Will be shown in the UI.
* @param string|null $documentation The human-readable doc-comment of this signature.
* @param ParameterInformation[]|null $parameters The parameters of this signature.
*/
public function __construct(string $label = null, string $documentation = null, array $parameters = null)
{
$this->label = $label;
$this->documentation = $documentation;
$this->parameters = $parameters;
}
}
23 changes: 22 additions & 1 deletion src/Server/TextDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace LanguageServer\Server;

use LanguageServer\{
CompletionProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
CompletionProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver, SignatureHelpProvider
};
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
Expand Down Expand Up @@ -73,6 +73,10 @@ class TextDocument
* @var \stdClass|null
*/
protected $composerLock;
/**
* @var SignatureHelpProvider
*/
protected $signatureHelpProvider;

/**
* @param PhpDocumentLoader $documentLoader
Expand All @@ -94,6 +98,7 @@ public function __construct(
$this->client = $client;
$this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->signatureHelpProvider = new SignatureHelpProvider($this->definitionResolver, $index);
$this->index = $index;
$this->composerJson = $composerJson;
$this->composerLock = $composerLock;
Expand Down Expand Up @@ -399,4 +404,20 @@ public function xdefinition(TextDocumentIdentifier $textDocument, Position $posi
return [new SymbolLocationInformation($descriptor, $def->symbolInformation->location)];
});
}

/**
* The signature help request is sent from the client to the server to request signature information
* at a given cursor position.
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
* @return Promise <SignatureHelp>
*/
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
{
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
return $this->signatureHelpProvider->provideSignature($document, $position);
});
}
}
111 changes: 111 additions & 0 deletions src/SignatureHelpProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
declare(strict_types = 1);

namespace LanguageServer;

use Microsoft\PhpParser\Node\DelimitedList\ArgumentExpressionList;
use Microsoft\PhpParser\Node\Expression\CallExpression;
use Microsoft\PhpParser\Node\Expression\ArgumentExpression;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
Range,
Position,
SignatureHelp,
SignatureInformation,
ParameterInformation
};

class SignatureHelpProvider
{
/**
* @var DefinitionResolver
*/
private $definitionResolver;

/**
* @var ReadableIndex
*/
private $index;

/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{
$this->definitionResolver = $definitionResolver;
$this->index = $index;
}

/**
* Get the short declaration for a callable (class modifiers, function keyword, etc are stripped)
*
* @param string $declaration
* @return string
*/
protected function getShortDeclaration(string $declaration): string
{
$parts = explode('(', $declaration, 2);
$name = array_reverse(explode(' ', trim($parts[0])))[0];
return $name . '(' . $parts[1];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method looks like it would ignore many edge cases, for example ( or spaces inside a default string value.
Why attempt to explode a declaration string when we have a parser available? I would add the wanted output to the Definition class

Copy link
Contributor Author

@vakata vakata Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am absolutely certain that this code does not ignore any edge cases - the first ( in a declaration is always after the function name. The function name is always the last token after all modifiers.

I am not sure how to change the current implementation in a robust way - all I need is to remove the modifiers and leave the function name, but keep all parameters (along with their type and default values). Doing this using the parser would require quite a huge block of code (from what I gather) especially since there is no way to pretty print the result.

Basically I would be reconstructing the already available declaration using string concatenation by inspecting the underlying AST just to remove the public / private / abstract / static / function keywords that precede the function name.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, maybe I'm was just unable to follow this code.

  • you split the declaration into two parts, the part before the ( (function name and keywords) and the rest
  • you trim whitespace from the first part (function name and keywords)
  • you split the first part by (function name and keywords) by spaces
  • you reverse the whole array
  • you take the first (initially last) element (function name)
  • you concatenate the function name, ( and the rest

I am pretty sure end() is faster to get the last array element than reversing the whole array and taking the first element

Copy link
Contributor Author

@vakata vakata Jul 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end requires a reference, which would mean creating a middle step and allocating a separate variable for the array. I am not very familiar with PHP internals, but I believe this means allocating storage in the heap, instead of the stack, so as far as optimization goes - I am not sure which will be faster and more memory efficient. Anyway - this really is a micro-optimization. I will create a few tests and change it if necessary.

EDIT: For what it is worth - I created a very crude test and at least for my configuration using a temporary array and end is on average about 3% slower.

}

/**
* Returns signature help for a specific cursor position in a document
*
* @param PhpDocument $doc The opened document
* @param Position $pos The cursor position
* @return SignatureHelp
*/
public function provideSignature(PhpDocument $doc, Position $pos) : SignatureHelp
{
$node = $doc->getNodeAtPosition($pos);
$arge = null;
while ($node &&
!($node instanceof ArgumentExpressionList) &&
!($node instanceof CallExpression) &&
$node->parent
) {
if ($node instanceof ArgumentExpression) {
$arge = $node;
}
$node = $node->parent;
}
if (!($node instanceof ArgumentExpressionList) &&
!($node instanceof CallExpression)
) {
return new SignatureHelp;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A signature without label is not valid according to the docblock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no label property in the SignatureHelp object - maybe I am missin something - please explain? This is supposed to be an empty result (no nested SignatureHelpInformation objects).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixfbecker please provide further information on how I can help improve this? I am still not sure what the issue is.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I confused it with the SignatureInformation class

}
$count = null;
if ($node instanceof ArgumentExpressionList) {
$count = 0;
foreach ($node->getElements() as $param) {
if ($param === $arge) {
break;
}
$count ++;
}
while ($node && !($node instanceof CallExpression) && $node->parent) {
$node = $node->parent;
}
if (!($node instanceof CallExpression)) {
return new SignatureHelp;
}
}
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node->callableExpression);
if (!$def) {
return new SignatureHelp;
}
return new SignatureHelp(
[
new SignatureInformation(
$this->getShortDeclaration($def->declarationLine),
$def->documentation,
$def->parameters
)
],
0,
$count !== null && $def->parameters !== null && $count < count($def->parameters) ? $count : null
);
}
}
5 changes: 4 additions & 1 deletion tests/LanguageServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
TextDocumentIdentifier,
InitializeResult,
ServerCapabilities,
CompletionOptions
CompletionOptions,
SignatureHelpOptions
};
use AdvancedJsonRpc;
use Webmozart\Glob\Glob;
Expand All @@ -40,6 +41,8 @@ public function testInitialize()
$serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions;
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(',','];
$serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true;
$serverCapabilities->xdependenciesProvider = true;
Expand Down
Loading