From 95a82dcdfeacb4966c9af67c73ae30b41e0a17ae Mon Sep 17 00:00:00 2001 From: Ivan Bozhanov Date: Sat, 15 Jul 2017 02:12:48 +0300 Subject: [PATCH] initial signature help commit --- fixtures/signature/funcClosed.php | 7 + fixtures/signature/funcNotClosed.php | 7 + fixtures/signature/methodClosed.php | 15 ++ fixtures/signature/methodNotClosed.php | 17 ++ fixtures/signature/staticClosed.php | 10 + fixtures/signature/staticNotClosed.php | 10 + src/Definition.php | 7 + src/DefinitionResolver.php | 13 + src/LanguageServer.php | 6 +- src/Protocol/ParameterInformation.php | 9 + src/Protocol/SignatureHelp.php | 11 + src/Protocol/SignatureInformation.php | 12 + src/Server/TextDocument.php | 15 +- src/SignatureHelpProvider.php | 92 ++++++++ tests/LanguageServerTest.php | 5 +- .../Server/TextDocument/SignatureHelpTest.php | 222 ++++++++++++++++++ 16 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 fixtures/signature/funcClosed.php create mode 100644 fixtures/signature/funcNotClosed.php create mode 100644 fixtures/signature/methodClosed.php create mode 100644 fixtures/signature/methodNotClosed.php create mode 100644 fixtures/signature/staticClosed.php create mode 100644 fixtures/signature/staticNotClosed.php create mode 100644 src/SignatureHelpProvider.php create mode 100644 tests/Server/TextDocument/SignatureHelpTest.php diff --git a/fixtures/signature/funcClosed.php b/fixtures/signature/funcClosed.php new file mode 100644 index 0000000..6012844 --- /dev/null +++ b/fixtures/signature/funcClosed.php @@ -0,0 +1,7 @@ +method(); + } +} + +$a = new HelpClass1; +$a->method(); \ No newline at end of file diff --git a/fixtures/signature/methodNotClosed.php b/fixtures/signature/methodNotClosed.php new file mode 100644 index 0000000..d5ce4cc --- /dev/null +++ b/fixtures/signature/methodNotClosed.php @@ -0,0 +1,17 @@ +method(1,1); + } +} +$a = new HelpClass2; +$a + ->method( + 1, + array(), diff --git a/fixtures/signature/staticClosed.php b/fixtures/signature/staticClosed.php new file mode 100644 index 0000000..301009f --- /dev/null +++ b/fixtures/signature/staticClosed.php @@ -0,0 +1,10 @@ +documentation = $this->getDocumentationFromNode($node); } + $def->parameters = []; + if (property_exists($node, 'parameters') && $node->parameters) { + foreach ($node->parameters->getElements() as $param) { + //var_dump($param); die(); + $def->parameters[] = new ParameterInformation( + $this->getDeclarationLineFromNode($param), + //$param->getName(), // TODO: rebuild this + $this->getDocumentationFromNode($param) + ); + } + } + return $def; } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 118dd93..0a3f9ff 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -9,7 +9,8 @@ use LanguageServer\Protocol\{ TextDocumentSyncKind, Message, InitializeResult, - CompletionOptions + CompletionOptions, + SignatureHelpOptions }; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; @@ -277,6 +278,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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; diff --git a/src/Protocol/ParameterInformation.php b/src/Protocol/ParameterInformation.php index 89b5e53..98c6c87 100644 --- a/src/Protocol/ParameterInformation.php +++ b/src/Protocol/ParameterInformation.php @@ -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; + } } diff --git a/src/Protocol/SignatureHelp.php b/src/Protocol/SignatureHelp.php index 407b25a..a99ef58 100644 --- a/src/Protocol/SignatureHelp.php +++ b/src/Protocol/SignatureHelp.php @@ -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; + } } diff --git a/src/Protocol/SignatureInformation.php b/src/Protocol/SignatureInformation.php index 77e152c..e1d4273 100644 --- a/src/Protocol/SignatureInformation.php +++ b/src/Protocol/SignatureInformation.php @@ -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; + } } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5a2819e..e936cf8 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Server; use LanguageServer\{ - CompletionProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver + CompletionProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver, SignatureHelpProvider }; use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\{ @@ -72,6 +72,10 @@ class TextDocument * @var \stdClass|null */ protected $composerLock; + /** + * @var SignatureHelpProvider + */ + protected $signatureHelpProvider; /** * @param PhpDocumentLoader $documentLoader @@ -93,6 +97,7 @@ class TextDocument $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; @@ -411,4 +416,12 @@ class TextDocument return [new SymbolLocationInformation($descriptor, $def->symbolInformation->location)]; }); } + + 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); + }); + } } diff --git a/src/SignatureHelpProvider.php b/src/SignatureHelpProvider.php new file mode 100644 index 0000000..5af3e97 --- /dev/null +++ b/src/SignatureHelpProvider.php @@ -0,0 +1,92 @@ +definitionResolver = $definitionResolver; + $this->index = $index; + } + + /** + * 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); + while ($node && + !($node instanceof ArgumentExpressionList) && + !($node instanceof CallExpression) && + $node->parent + ) { + $node = $node->parent; + } + if (!($node instanceof ArgumentExpressionList) && + !($node instanceof CallExpression) + ) { + return new SignatureHelp; + } + $count = 0; + if ($node instanceof ArgumentExpressionList) { + foreach ($node->getElements() as $param) { + $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; + } + $params = array_map(function ($v) { + return $v->label; + }, $def->parameters); + return new SignatureHelp( + [ + new SignatureInformation( + trim(str_replace(['public', 'protected', 'private', 'function', 'static'], '', $def->declarationLine)), + $def->documentation, + $def->parameters + ) + ], + 0, + $count < count($def->parameters) ? $count : null + ); + } +} diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 5d03451..95967ac 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -14,7 +14,8 @@ use LanguageServer\Protocol\{ TextDocumentIdentifier, InitializeResult, ServerCapabilities, - CompletionOptions + CompletionOptions, + SignatureHelpOptions }; use AdvancedJsonRpc; use Webmozart\Glob\Glob; @@ -41,6 +42,8 @@ class LanguageServerTest extends TestCase $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; diff --git a/tests/Server/TextDocument/SignatureHelpTest.php b/tests/Server/TextDocument/SignatureHelpTest.php new file mode 100644 index 0000000..76613e1 --- /dev/null +++ b/tests/Server/TextDocument/SignatureHelpTest.php @@ -0,0 +1,222 @@ +loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); + $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); + } + + public function testMethodClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/methodClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(9, 22) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } + + public function testMethodClosedReference() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/methodClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(14, 11) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } + + public function testMethodNotClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/methodNotClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(9, 22) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } + + public function testMethodNotClosedReference() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/methodNotClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(14, 14) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } + + public function testFuncClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/funcClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(6, 10) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'helpFunc1(int $count = 0)', + null, + [ + new ParameterInformation('int $count = 0') + ] + ) + ] + ), $result); + } + + public function testFuncNotClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/funcNotClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(6, 10) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'helpFunc2(int $count = 0)', + null, + [ + new ParameterInformation('int $count = 0') + ] + ) + ] + ), $result); + } + + public function testStaticClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/staticClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(9, 19) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } + + public function testStaticNotClosed() + { + $completionUri = pathToUri(__DIR__ . '/../../../fixtures/signature/staticNotClosed.php'); + $this->loader->open($completionUri, file_get_contents($completionUri)); + $result = $this->textDocument->signatureHelp( + new TextDocumentIdentifier($completionUri), + new Position(9, 19) + )->wait(); + + $this->assertEquals(new SignatureHelp( + [ + new SignatureInformation( + 'method(string $param = "")', + null, + [ + new ParameterInformation('string $param = ""') + ] + ) + ] + ), $result); + } +}