Skip to content
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