Initial implemention for code completion
parent
b16674d394
commit
d6200fc07a
|
@ -0,0 +1,81 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
public function __construct(PhpDocument $phpDocument)
|
||||||
|
{
|
||||||
|
$this->phpDocument = $phpDocument;
|
||||||
|
$this->lines = explode("\n", $this->phpDocument->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReplacementRange(): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,57 @@
|
||||||
|
<?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 (!$this->isValidContext($context)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidContext(CompletionContext $context)
|
||||||
|
{
|
||||||
|
$line = $context->getLine($context->getPosition()->line);
|
||||||
|
if (empty($line)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$range = $context->getReplacementRange($context);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?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)
|
||||||
|
{
|
||||||
|
$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,106 @@
|
||||||
|
<?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)
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,11 +11,10 @@ use LanguageServer\Protocol\{
|
||||||
Message,
|
Message,
|
||||||
MessageType,
|
MessageType,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
SymbolInformation
|
CompletionOptions
|
||||||
};
|
};
|
||||||
use AdvancedJsonRpc;
|
use AdvancedJsonRpc;
|
||||||
use Sabre\Event\Loop;
|
use Sabre\Event\Loop;
|
||||||
use JsonMapper;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
|
@ -87,6 +86,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
|
|
||||||
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
||||||
$this->workspace = new Server\Workspace($this->project, $this->client);
|
$this->workspace = new Server\Workspace($this->project, $this->client);
|
||||||
|
$this->completionItem = new Server\CompletionItemResolver($this->project);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,7 +122,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->referencesProvider = true;
|
$serverCapabilities->referencesProvider = true;
|
||||||
// Support "Hover"
|
// Support "Hover"
|
||||||
$serverCapabilities->hoverProvider = true;
|
$serverCapabilities->hoverProvider = true;
|
||||||
|
// Support code completion
|
||||||
|
$completionOptions = new CompletionOptions();
|
||||||
|
$completionOptions->resolveProvider = true;
|
||||||
|
$completionOptions->triggerCharacters = [':', '$', '>'];
|
||||||
|
$serverCapabilities->completionProvider = $completionOptions;
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
|
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Position, TextEdit};
|
||||||
use LanguageServer\NodeVisitor\{
|
use LanguageServer\NodeVisitor\{
|
||||||
NodeAtPositionFinder,
|
NodeAtPositionFinder,
|
||||||
ReferencesAdder,
|
ReferencesAdder,
|
||||||
|
@ -17,6 +17,7 @@ use PhpParser\{Error, Node, NodeTraverser, Parser};
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
use phpDocumentor\Reflection\DocBlockFactory;
|
use phpDocumentor\Reflection\DocBlockFactory;
|
||||||
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
|
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
|
||||||
|
use LanguageServer\Completion\CompletionReporter;
|
||||||
|
|
||||||
class PhpDocument
|
class PhpDocument
|
||||||
{
|
{
|
||||||
|
@ -92,6 +93,12 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
private $symbols;
|
private $symbols;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var \LanguageServer\Completion\CompletionReporter
|
||||||
|
*/
|
||||||
|
private $completionReporter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
* @param string $content The content of the document
|
* @param string $content The content of the document
|
||||||
|
@ -132,6 +139,8 @@ class PhpDocument
|
||||||
public function updateContent(string $content)
|
public function updateContent(string $content)
|
||||||
{
|
{
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
|
$this->completionReporter = new CompletionReporter($this);
|
||||||
|
|
||||||
$stmts = null;
|
$stmts = null;
|
||||||
$errors = [];
|
$errors = [];
|
||||||
try {
|
try {
|
||||||
|
@ -229,6 +238,17 @@ class PhpDocument
|
||||||
return Formatter::format($this->content, $this->uri);
|
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.
|
* Returns this document's text content.
|
||||||
*
|
*
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The kind of a completion entry.
|
* The kind of a completion entry.
|
||||||
*/
|
*/
|
||||||
|
@ -24,4 +26,40 @@ abstract class CompletionItemKind {
|
||||||
const COLOR = 16;
|
const COLOR = 16;
|
||||||
const FILE = 17;
|
const FILE = 17;
|
||||||
const REFERENCE = 18;
|
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.
|
* The characters that trigger completion automatically.
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string[]|null
|
||||||
*/
|
*/
|
||||||
public $triggerCharacters;
|
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);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,14 @@ class LanguageServerTest extends TestCase
|
||||||
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
||||||
'documentSymbolProvider' => true,
|
'documentSymbolProvider' => true,
|
||||||
'hoverProvider' => true,
|
'hoverProvider' => true,
|
||||||
'completionProvider' => null,
|
'completionProvider' => (object)[
|
||||||
|
'resolveProvider' => true,
|
||||||
|
'triggerCharacters' => [
|
||||||
|
':',
|
||||||
|
'$',
|
||||||
|
'>',
|
||||||
|
]
|
||||||
|
],
|
||||||
'signatureHelpProvider' => null,
|
'signatureHelpProvider' => null,
|
||||||
'definitionProvider' => true,
|
'definitionProvider' => true,
|
||||||
'referencesProvider' => true,
|
'referencesProvider' => true,
|
||||||
|
|
Loading…
Reference in New Issue