Merge 9388edd578
into 25f300c157
commit
b97e5fc80d
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion;
|
||||
|
||||
use LanguageServer\PhpDocument;
|
||||
use LanguageServer\Protocol\ {
|
||||
Range,
|
||||
Position
|
||||
};
|
||||
|
||||
class CompletionContext
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var \LanguageServer\Protocol\Position
|
||||
*/
|
||||
private $position;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var \LanguageServer\PhpDocument
|
||||
*/
|
||||
private $phpDocument;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $lines;
|
||||
|
||||
/**
|
||||
* @var \LanguageServer\Protocol\Range
|
||||
*/
|
||||
private $replacementRange;
|
||||
|
||||
public function __construct(PhpDocument $phpDocument)
|
||||
{
|
||||
$this->phpDocument = $phpDocument;
|
||||
$this->lines = explode("\n", $this->phpDocument->getContent());
|
||||
}
|
||||
|
||||
public function getReplacementRange(): Range
|
||||
{
|
||||
return $this->replacementRange;
|
||||
}
|
||||
|
||||
private function calculateReplacementRange(): Range
|
||||
{
|
||||
$line = $this->getLine($this->position->line);
|
||||
if (!empty($line)) {
|
||||
// modified regexp from http://php.net/manual/en/language.variables.basics.php
|
||||
if (preg_match_all('@\$?[a-zA-Z_\x7f-\xff]?[a-zA-Z0-9_\x7f-\xff]*@', $line, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
if (!empty($match[0])) {
|
||||
$start = new Position($this->position->line, $match[1]);
|
||||
$end = new Position($this->position->line, $match[1] + strlen($match[0]));
|
||||
$range = new Range($start, $end);
|
||||
if ($range->includes($this->position)) {
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Range($this->position, $this->position);
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(Position $position)
|
||||
{
|
||||
$this->position = $position;
|
||||
$this->replacementRange = $this->calculateReplacementRange();
|
||||
}
|
||||
|
||||
public function isObjectContext()
|
||||
{
|
||||
$line = $this->getLine($this->getPosition()->line);
|
||||
if (empty($line)) {
|
||||
return false;
|
||||
}
|
||||
$range = $this->getReplacementRange();
|
||||
if (preg_match_all('@(\$this->|self::)@', $line, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
if (($match[1] + strlen($match[0])) === $range->start->character) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getLine(int $line)
|
||||
{
|
||||
if (count($this->lines) <= $line) {
|
||||
return null;
|
||||
}
|
||||
return $this->lines[$line];
|
||||
}
|
||||
|
||||
public function getPhpDocument()
|
||||
{
|
||||
return $this->phpDocument;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion;
|
||||
|
||||
use LanguageServer\Protocol\ {
|
||||
CompletionItem,
|
||||
Range,
|
||||
Position,
|
||||
TextEdit,
|
||||
CompletionItemKind,
|
||||
CompletionList
|
||||
};
|
||||
use LanguageServer\Completion\Strategies\ {
|
||||
KeywordsStrategy,
|
||||
VariablesStrategy,
|
||||
ClassMembersStrategy,
|
||||
GlobalElementsStrategy
|
||||
};
|
||||
use LanguageServer\PhpDocument;
|
||||
use PhpParser\Node;
|
||||
|
||||
class CompletionReporter
|
||||
{
|
||||
/**
|
||||
* @var \LanguageServer\Protocol\CompletionItem
|
||||
*/
|
||||
private $completionItems;
|
||||
|
||||
/**
|
||||
* @var \LanguageServer\Completion\ICompletionStrategy
|
||||
*/
|
||||
private $strategies;
|
||||
|
||||
private $context;
|
||||
|
||||
public function __construct(PhpDocument $phpDocument)
|
||||
{
|
||||
$this->context = new CompletionContext($phpDocument);
|
||||
$this->strategies = [
|
||||
new KeywordsStrategy(),
|
||||
new VariablesStrategy(),
|
||||
new ClassMembersStrategy(),
|
||||
new GlobalElementsStrategy()
|
||||
];
|
||||
}
|
||||
|
||||
public function complete(Position $position)
|
||||
{
|
||||
$this->completionItems = [];
|
||||
$this->context->setPosition($position);
|
||||
foreach ($this->strategies as $strategy) {
|
||||
$strategy->apply($this->context, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function reportByNode(Node $node, Range $editRange, string $fqn = null)
|
||||
{
|
||||
if (!$node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof \PhpParser\Node\Stmt\Property) {
|
||||
foreach ($node->props as $prop) {
|
||||
$this->reportByNode($prop, $editRange, $fqn);
|
||||
}
|
||||
} else if ($node instanceof \PhpParser\Node\Stmt\ClassConst) {
|
||||
foreach ($node->consts as $const) {
|
||||
$this->reportByNode($const, $editRange, $fqn);
|
||||
}
|
||||
} else {
|
||||
$this->report($node->name, CompletionItemKind::fromNode($node), $node->name, $editRange, $fqn);
|
||||
}
|
||||
}
|
||||
|
||||
public function report(string $label, int $kind, string $insertText, Range $editRange, string $fqn = null)
|
||||
{
|
||||
$item = new CompletionItem();
|
||||
$item->label = $label;
|
||||
$item->kind = $kind;
|
||||
$item->textEdit = new TextEdit($editRange, $insertText);
|
||||
$item->data = $fqn;
|
||||
|
||||
$this->completionItems[] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return CompletionList
|
||||
*/
|
||||
public function getCompletionList(): CompletionList
|
||||
{
|
||||
$completionList = new CompletionList();
|
||||
$completionList->isIncomplete = false;
|
||||
$completionList->items = $this->completionItems;
|
||||
return $completionList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion;
|
||||
|
||||
interface ICompletionStrategy
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @param \LanguageServer\Completion\CompletionContext $context
|
||||
* @param \LanguageServer\Completion\CompletionReporter $reporter
|
||||
*/
|
||||
public function apply(CompletionContext $context, CompletionReporter $reporter);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion\Strategies;
|
||||
|
||||
use LanguageServer\Completion\ {
|
||||
CompletionContext,
|
||||
CompletionReporter,
|
||||
ICompletionStrategy
|
||||
};
|
||||
use LanguageServer\Protocol\Range;
|
||||
|
||||
class ClassMembersStrategy implements ICompletionStrategy
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(CompletionContext $context, CompletionReporter $reporter)
|
||||
{
|
||||
if (!$context->isObjectContext()) {
|
||||
return;
|
||||
}
|
||||
$range = $context->getReplacementRange();
|
||||
$nodes = $context->getPhpDocument()->getDefinitions();
|
||||
foreach ($nodes as $fqn => $node) {
|
||||
if ($node instanceof \PhpParser\Node\Stmt\ClassLike) {
|
||||
$nodeRange = Range::fromNode($node);
|
||||
if ($nodeRange->includes($context->getPosition())) {
|
||||
foreach ($nodes as $childFqn => $child) {
|
||||
if (stripos($childFqn, $fqn) === 0 && $childFqn !== $fqn) {
|
||||
$reporter->reportByNode($child, $range, $childFqn);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion\Strategies;
|
||||
|
||||
use LanguageServer\Completion\ {
|
||||
CompletionContext,
|
||||
CompletionReporter,
|
||||
ICompletionStrategy
|
||||
};
|
||||
use LanguageServer\Protocol\CompletionItemKind;
|
||||
use LanguageServer\Protocol\SymbolInformation;
|
||||
use LanguageServer\Protocol\SymbolKind;
|
||||
|
||||
class GlobalElementsStrategy implements ICompletionStrategy
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(CompletionContext $context, CompletionReporter $reporter)
|
||||
{
|
||||
if ($context->isObjectContext()) {
|
||||
return;
|
||||
}
|
||||
$range = $context->getReplacementRange($context);
|
||||
$project = $context->getPhpDocument()->project;
|
||||
foreach ($project->getSymbols() as $fqn => $symbol) {
|
||||
if ($this->isValid($symbol)) {
|
||||
$kind = CompletionItemKind::fromSymbol($symbol->kind);
|
||||
$reporter->report($symbol->name, $kind, $symbol->name, $range, $fqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function isValid(SymbolInformation $symbol)
|
||||
{
|
||||
return $symbol->kind == SymbolKind::CLASS_
|
||||
|| $symbol->kind == SymbolKind::INTERFACE
|
||||
|| $symbol->kind == SymbolKind::FUNCTION;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion\Strategies;
|
||||
|
||||
use LanguageServer\Protocol\ {
|
||||
Range,
|
||||
CompletionItemKind
|
||||
};
|
||||
use LanguageServer\Completion\ {
|
||||
ICompletionStrategy,
|
||||
CompletionContext,
|
||||
CompletionReporter
|
||||
};
|
||||
|
||||
class KeywordsStrategy implements ICompletionStrategy
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
const KEYWORDS = [
|
||||
"abstract",
|
||||
"and",
|
||||
"array",
|
||||
"as",
|
||||
"break",
|
||||
"callable",
|
||||
"case",
|
||||
"catch",
|
||||
"class",
|
||||
"clone",
|
||||
"const",
|
||||
"continue",
|
||||
"declare",
|
||||
"default",
|
||||
"die",
|
||||
"do",
|
||||
"echo",
|
||||
"else",
|
||||
"elseif",
|
||||
"empty",
|
||||
"enddeclare",
|
||||
"endfor",
|
||||
"endforeach",
|
||||
"endif",
|
||||
"endswitch",
|
||||
"endwhile",
|
||||
"eval",
|
||||
"exit",
|
||||
"extends",
|
||||
"false",
|
||||
"final",
|
||||
"finally",
|
||||
"for",
|
||||
"foreach",
|
||||
"function",
|
||||
"global",
|
||||
"goto",
|
||||
"if",
|
||||
"implements",
|
||||
"include",
|
||||
"include_once",
|
||||
"instanceof",
|
||||
"insteadof",
|
||||
"interface",
|
||||
"isset",
|
||||
"list",
|
||||
"namespace",
|
||||
"new",
|
||||
"null",
|
||||
"or",
|
||||
"parent",
|
||||
"print",
|
||||
"private",
|
||||
"protected",
|
||||
"public",
|
||||
"require",
|
||||
"require_once",
|
||||
"return",
|
||||
"self",
|
||||
"static",
|
||||
"switch",
|
||||
"throw",
|
||||
"trait",
|
||||
"true",
|
||||
"try",
|
||||
"unset",
|
||||
"use",
|
||||
"var",
|
||||
"while",
|
||||
"xor",
|
||||
"yield"
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(CompletionContext $context, CompletionReporter $reporter)
|
||||
{
|
||||
if ($context->isObjectContext()) {
|
||||
return;
|
||||
}
|
||||
$range = $context->getReplacementRange();
|
||||
foreach (self::KEYWORDS as $keyword) {
|
||||
$reporter->report($keyword, CompletionItemKind::KEYWORD, $keyword, $range);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Completion\Strategies;
|
||||
|
||||
use LanguageServer\Protocol\ {
|
||||
CompletionItemKind,
|
||||
Range,
|
||||
SymbolKind,
|
||||
SymbolInformation
|
||||
};
|
||||
use LanguageServer\Completion\ {
|
||||
ICompletionStrategy,
|
||||
CompletionContext,
|
||||
CompletionReporter
|
||||
};
|
||||
|
||||
class VariablesStrategy implements ICompletionStrategy
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(CompletionContext $context, CompletionReporter $reporter)
|
||||
{
|
||||
$range = $context->getReplacementRange();
|
||||
$symbols = $context->getPhpDocument()->getSymbols();
|
||||
$contextSymbol = null;
|
||||
foreach ($symbols as $symbol) {
|
||||
if ($this->isValid($symbol) && $symbol->location->range->includes($context->getPosition())) {
|
||||
$contextSymbol = $symbol;
|
||||
}
|
||||
}
|
||||
|
||||
if ($contextSymbol !== null) {
|
||||
$content = '';
|
||||
$start = $contextSymbol->location->range->start;
|
||||
$end = $contextSymbol->location->range->end;
|
||||
for ($i = $start->line; $i <= $end->line; $i++) {
|
||||
$content .= $context->getLine($i);
|
||||
}
|
||||
} else {
|
||||
$content = $context->getPhpDocument()->getContent();
|
||||
}
|
||||
|
||||
if (preg_match_all('@\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*@', $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$variables = [];
|
||||
foreach ($matches[0] as $match) {
|
||||
$variables[] = $match[0];
|
||||
}
|
||||
|
||||
$variables = array_unique($variables);
|
||||
|
||||
foreach ($variables as $variable) {
|
||||
$reporter->report($variable, CompletionItemKind::VARIABLE, $variable, $range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function isValid(SymbolInformation $symbol)
|
||||
{
|
||||
return $symbol->kind === SymbolKind::FUNCTION || $symbol->kind === SymbolKind::METHOD;
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ use LanguageServer\Protocol\{
|
|||
Message,
|
||||
MessageType,
|
||||
InitializeResult,
|
||||
SymbolInformation
|
||||
CompletionOptions
|
||||
};
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Loop;
|
||||
|
@ -89,6 +89,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
|
||||
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
||||
$this->workspace = new Server\Workspace($this->project, $this->client);
|
||||
$this->completionItem = new Server\CompletionItemResolver($this->project);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,7 +124,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$serverCapabilities->referencesProvider = true;
|
||||
// Support "Hover"
|
||||
$serverCapabilities->hoverProvider = true;
|
||||
|
||||
// Support code completion
|
||||
$completionOptions = new CompletionOptions();
|
||||
$completionOptions->resolveProvider = true;
|
||||
$completionOptions->triggerCharacters = [':', '$', '>'];
|
||||
$serverCapabilities->completionProvider = $completionOptions;
|
||||
return new InitializeResult($serverCapabilities);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
|
||||
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Position, TextEdit};
|
||||
use LanguageServer\NodeVisitor\{
|
||||
NodeAtPositionFinder,
|
||||
ReferencesAdder,
|
||||
|
@ -17,6 +17,7 @@ use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
|
|||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
|
||||
use LanguageServer\Completion\CompletionReporter;
|
||||
|
||||
class PhpDocument
|
||||
{
|
||||
|
@ -92,6 +93,12 @@ class PhpDocument
|
|||
*/
|
||||
private $symbols;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var \LanguageServer\Completion\CompletionReporter
|
||||
*/
|
||||
private $completionReporter;
|
||||
|
||||
/**
|
||||
* @param string $uri The URI of the document
|
||||
* @param string $content The content of the document
|
||||
|
@ -132,6 +139,8 @@ class PhpDocument
|
|||
public function updateContent(string $content)
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->completionReporter = new CompletionReporter($this);
|
||||
|
||||
$stmts = null;
|
||||
|
||||
$errorHandler = new ErrorHandler\Collecting;
|
||||
|
@ -222,6 +231,17 @@ class PhpDocument
|
|||
return Formatter::format($this->content, $this->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Position $position
|
||||
*
|
||||
* @return \LanguageServer\Protocol\CompletionList
|
||||
*/
|
||||
public function complete(Position $position)
|
||||
{
|
||||
$this->completionReporter->complete($position);
|
||||
return $this->completionReporter->getCompletionList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this document's text content.
|
||||
*
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
/**
|
||||
* The kind of a completion entry.
|
||||
*/
|
||||
|
@ -25,4 +27,40 @@ abstract class CompletionItemKind
|
|||
const COLOR = 16;
|
||||
const FILE = 17;
|
||||
const REFERENCE = 18;
|
||||
|
||||
public static function fromSymbol(int $symbolKind)
|
||||
{
|
||||
$symbolCompletionKindMap = [
|
||||
SymbolKind::CLASS_ => CompletionItemKind::_CLASS,
|
||||
SymbolKind::INTERFACE => CompletionItemKind::INTERFACE,
|
||||
SymbolKind::FUNCTION => CompletionItemKind::FUNCTION,
|
||||
SymbolKind::METHOD => CompletionItemKind::METHOD,
|
||||
SymbolKind::FIELD => CompletionItemKind::FIELD,
|
||||
SymbolKind::CONSTRUCTOR => CompletionItemKind::CONSTRUCTOR,
|
||||
SymbolKind::VARIABLE => CompletionItemKind::VARIABLE,
|
||||
];
|
||||
|
||||
return $symbolCompletionKindMap[$symbolKind];
|
||||
}
|
||||
|
||||
public static function fromNode(Node $node)
|
||||
{
|
||||
$nodeCompletionKindMap = [
|
||||
Node\Stmt\Class_::class => CompletionItemKind::_CLASS,
|
||||
Node\Stmt\Trait_::class => CompletionItemKind::_CLASS,
|
||||
Node\Stmt\Interface_::class => CompletionItemKind::INTERFACE,
|
||||
|
||||
Node\Stmt\Function_::class => CompletionItemKind::FUNCTION,
|
||||
Node\Stmt\ClassMethod::class => CompletionItemKind::METHOD,
|
||||
Node\Stmt\PropertyProperty::class => CompletionItemKind::PROPERTY,
|
||||
Node\Const_::class => CompletionItemKind::FIELD
|
||||
];
|
||||
$class = get_class($node);
|
||||
if (!isset($nodeCompletionKindMap[$class])) {
|
||||
throw new Exception("Not a declaration node: $class");
|
||||
}
|
||||
|
||||
return $nodeCompletionKindMap[$class];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class CompletionOptions
|
|||
/**
|
||||
* The characters that trigger completion automatically.
|
||||
*
|
||||
* @var string|null
|
||||
* @var string[]|null
|
||||
*/
|
||||
public $triggerCharacters;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Server;
|
||||
|
||||
use LanguageServer\Protocol\ {
|
||||
CompletionItem,
|
||||
TextEdit
|
||||
};
|
||||
use PhpParser\Node;
|
||||
use LanguageServer\Project;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
|
||||
class CompletionItemResolver
|
||||
{
|
||||
/**
|
||||
* @var \LanguageServer\Project
|
||||
*/
|
||||
private $project;
|
||||
|
||||
/**
|
||||
* @var \phpDocumentor\Reflection\DocBlockFactory
|
||||
*/
|
||||
private $docBlockFactory;
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* The request is sent from the client to the server to resolve additional information for a given completion item.
|
||||
*
|
||||
* @param string $label
|
||||
* @param int $kind
|
||||
* @param TextEdit $textEdit
|
||||
* @param string $data
|
||||
*
|
||||
* @return \LanguageServer\Protocol\CompletionItem
|
||||
*/
|
||||
public function resolve($label, $kind, $textEdit, $data)
|
||||
{
|
||||
$item = new CompletionItem();
|
||||
$item->label = $label;
|
||||
$item->kind = $kind;
|
||||
$item->textEdit = $textEdit;
|
||||
|
||||
if (!isset($data)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$fqn = $data;
|
||||
$phpDocument = $this->project->getDefinitionDocument($fqn);
|
||||
if (!$phpDocument) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$node = $phpDocument->getDefinitionByFqn($fqn);
|
||||
if (!isset($node)) {
|
||||
return $item;
|
||||
}
|
||||
$item->detail = $this->generateItemDetails($node);
|
||||
$item->documentation = $this->getDocumentation($node);
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function generateItemDetails(Node $node)
|
||||
{
|
||||
if ($node instanceof \PhpParser\Node\FunctionLike) {
|
||||
return $this->generateFunctionSignature($node);
|
||||
}
|
||||
if (isset($node->namespacedName)) {
|
||||
return '\\' . ((string) $node->namespacedName);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function generateFunctionSignature(\PhpParser\Node\FunctionLike $node)
|
||||
{
|
||||
$params = [];
|
||||
foreach ($node->getParams() as $param) {
|
||||
$label = $param->type ? ((string) $param->type) . ' ' : '';
|
||||
$label .= '$' . $param->name;
|
||||
$params[] = $label;
|
||||
}
|
||||
$signature = '(' . implode(', ', $params) . ')';
|
||||
if ($node->getReturnType()) {
|
||||
$signature .= ': ' . $node->getReturnType();
|
||||
}
|
||||
return $signature;
|
||||
}
|
||||
|
||||
private function getDocumentation(Node $node)
|
||||
{
|
||||
// Get the documentation string
|
||||
$contents = '';
|
||||
$docBlock = $node->getAttribute('docBlock');
|
||||
if ($docBlock !== null) {
|
||||
$contents .= $docBlock->getSummary() . "\n\n";
|
||||
$contents .= $docBlock->getDescription();
|
||||
}
|
||||
return $contents;
|
||||
}
|
||||
}
|
|
@ -223,4 +223,17 @@ class TextDocument
|
|||
|
||||
return new Hover($contents, $range);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument
|
||||
* @param \LanguageServer\Protocol\Position $position
|
||||
*
|
||||
* @return \LanguageServer\Protocol\CompletionList
|
||||
*/
|
||||
public function completion(TextDocumentIdentifier $textDocument, Position $position)
|
||||
{
|
||||
$document = $this->project->getDocument($textDocument->uri);
|
||||
return $document->complete($position);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,7 +33,14 @@ class LanguageServerTest extends TestCase
|
|||
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
||||
'documentSymbolProvider' => true,
|
||||
'hoverProvider' => true,
|
||||
'completionProvider' => null,
|
||||
'completionProvider' => (object)[
|
||||
'resolveProvider' => true,
|
||||
'triggerCharacters' => [
|
||||
':',
|
||||
'$',
|
||||
'>',
|
||||
]
|
||||
],
|
||||
'signatureHelpProvider' => null,
|
||||
'definitionProvider' => true,
|
||||
'referencesProvider' => true,
|
||||
|
|
Loading…
Reference in New Issue