diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 118dd93..4e783ad 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,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $serverCapabilities->completionProvider = new CompletionOptions; $serverCapabilities->completionProvider->resolveProvider = false; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(); + $serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ',']; + // Support global references $serverCapabilities->xworkspaceReferencesProvider = true; $serverCapabilities->xdefinitionProvider = true; diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5a2819e..37ee658 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, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver }; use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\{ @@ -58,6 +58,8 @@ class TextDocument */ protected $completionProvider; + protected $signatureHelpProvider; + /** * @var ReadableIndex */ @@ -93,6 +95,7 @@ class TextDocument $this->client = $client; $this->definitionResolver = $definitionResolver; $this->completionProvider = new CompletionProvider($this->definitionResolver, $index); + $this->signatureHelpProvider = new SignatureHelpProvider($this->definitionResolver, $index, $documentLoader); $this->index = $index; $this->composerJson = $composerJson; $this->composerLock = $composerLock; @@ -250,6 +253,22 @@ class TextDocument }); } + /** + * The goto definition request is sent from the client to the server to resolve the definition location of a symbol + * at a given text document position. + * + * @param TextDocumentIdentifier $textDocument The text document + * @param Position $position The position inside the text document + * @return Promise + */ + public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise + { + return coroutine(function () use ($textDocument, $position) { + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); + return $this->signatureHelpProvider->getSignatureHelp($document, $position); + }); + } + /** * The goto definition request is sent from the client to the server to resolve the definition location of a symbol * at a given text document position. diff --git a/src/SignatureHelpProvider.php b/src/SignatureHelpProvider.php new file mode 100644 index 0000000..862c0da --- /dev/null +++ b/src/SignatureHelpProvider.php @@ -0,0 +1,194 @@ +definitionResolver = $definitionResolver; + $this->index = $index; + $this->documentLoader = $documentLoader; + } + + public function getSignatureHelp(PhpDocument $doc, Position $position): SignatureHelp + { + // Find the node under the cursor + $node = $doc->getNodeAtPosition($position); + + $fqn = null; + + // First find the node that the call belongs to + if ($node instanceof Node\DelimitedList\ArgumentExpressionList) { + $argumentExpressionList = $node; + if ($node->parent instanceof Node\Expression\ObjectCreationExpression) { + $node = $node->parent->classTypeDesignator; + if (!$node instanceof Node\QualifiedName) { + return new SignatureHelp(); + } + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + $fqn = "{$fqn}->__construct()"; + } else { + $node = $node->parent->getFirstChildNode( + Node\Expression\MemberAccessExpression::class, + Node\Expression\ScopedPropertyAccessExpression::class, + Node\QualifiedName::class + ); + } + } elseif ($node instanceof Node\Expression\CallExpression) { + $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); + $node = $node->getFirstChildNode( + Node\Expression\MemberAccessExpression::class, + Node\Expression\ScopedPropertyAccessExpression::class, + Node\QualifiedName::class + ); + } elseif ($node instanceof Node\Expression\ObjectCreationExpression) { + $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); + //$node = $node->getFirstChildNode(Node\QualifiedName::class); + $node = $node->classTypeDesignator; + if (!$node instanceof Node\QualifiedName) { + return new SignatureHelp(); + } + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); + $fqn = "{$fqn}->__construct()"; + } else { + $node = null; + } + + if (!$node) { + return new SignatureHelp(); + } + + // Now find the definition of the call + $fqn = $fqn ?: DefinitionResolver::getDefinedFqn($node); + if ($fqn) { + $def = $this->index->getDefinition($fqn); + } else { + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); + } + + if (!$def) { + return new SignatureHelp(); + } + + $activeParam = $argumentExpressionList + ? $this->findActiveParameter($argumentExpressionList, $position, $doc) + : 0; + + $doc = $this->documentLoader->get($def->symbolInformation->location->uri); + if (!$doc) { + return new SignatureHelp(); + } + $node = $doc->getNodeAtPosition($def->symbolInformation->location->range->start); + $params = $this->getParameters($node, $doc); + $label = $this->getLabel($node, $params, $doc); + $signatureInformation = new SignatureInformation(); + $signatureInformation->label = $label; + $signatureInformation->parameters = $params; + $signatureHelp = new SignatureHelp(); + $signatureHelp->signatures = [$signatureInformation]; + $signatureHelp->activeParameter = $activeParam; + return $signatureHelp; + } + + /** + * @param Node\MethodDeclaration|Node\Statement\FunctionDeclaration $node + */ + private function getLabel($node, array $params, PhpDocument $doc): string + { + //$label = $node->getName() . '('; + $label = '('; + if ($params) { + foreach ($params as $param) { + $label .= $param->label . ', '; + } + $label = substr($label, 0, -2); + } + $label .= ')'; + /* + if ($node->returnType) { + $label .= ': '; + if ($node->returnType instanceof QualifiedName) { + $label .= $node->returnType->getResolvedName(); + } else { + $label .= $node->returnType->getText($doc->getContent()); + } + } + */ + return $label; + } + + /** + * @param Node\MethodDeclaration|Node\Statement\FunctionDeclaration $node + */ + private function getParameters($node, PhpDocument $doc): array + { + $params = []; + if ($node->parameters) { + foreach ($node->parameters->getElements() as $element) { + $param = (string) $this->definitionResolver->getTypeFromNode($element); + $param .= ' ' . $element->variableName->getText($doc->getContent()); + if ($element->default) { + $param .= ' = ' . $element->default->getText($doc->getContent()); + } + $info = new ParameterInformation(); + $info->label = $param; + $info->documentation = $this->definitionResolver->getDocumentationFromNode($element); + $params[] = $info; + } + } + return $params; + } + + private function findActiveParameter( + Node\DelimitedList\ArgumentExpressionList $argumentExpressionList, + Position $position, + PhpDocument $doc + ): int { + $args = $argumentExpressionList->children; + $i = 0; + $found = null; + foreach ($args as $arg) { + if ($arg instanceof Node) { + $start = $arg->getFullStart(); + $end = $arg->getEndPosition(); + ++$i; + } else { + $start = $arg->fullStart; + $end = $start + $arg->length; + } + $offset = $position->toOffset($doc->getContent()); + if ($offset >= $start && $offset <= $end) { + $found = $i; + break; + } + } + if (is_null($found)) { + $found = $i; + } + return $found; + } +}