diff --git a/fixtures/completion/html.php b/fixtures/completion/html.php
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/completion/html_with_prefix.php b/fixtures/completion/html_with_prefix.php
new file mode 100644
index 0000000..9318418
--- /dev/null
+++ b/fixtures/completion/html_with_prefix.php
@@ -0,0 +1 @@
+<
diff --git a/fixtures/completion/keywords.php b/fixtures/completion/keywords.php
new file mode 100644
index 0000000..76fc7cb
--- /dev/null
+++ b/fixtures/completion/keywords.php
@@ -0,0 +1,3 @@
+',
+ '__halt_compiler',
+ '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',
+ 'final',
+ 'finally',
+ 'for',
+ 'foreach',
+ 'function',
+ 'global',
+ 'goto',
+ 'if',
+ 'implements',
+ 'include',
+ 'include_once',
+ 'instanceof',
+ 'insteadof',
+ 'interface',
+ 'isset',
+ 'list',
+ 'namespace',
+ 'new',
+ 'or',
+ 'print',
+ 'private',
+ 'protected',
+ 'public',
+ 'require',
+ 'require_once',
+ 'return',
+ 'static',
+ 'switch',
+ 'throw',
+ 'trait',
+ 'try',
+ 'unset',
+ 'use',
+ 'var',
+ 'while',
+ 'xor',
+ 'yield'
+ ];
+
/**
* @var DefinitionResolver
*/
@@ -38,12 +111,12 @@ class CompletionProvider
* Returns suggestions for a specific cursor position in a document
*
* @param PhpDocument $document The opened document
- * @param Position $position The cursor position
+ * @param Position $pos The cursor position
* @return CompletionItem[]
*/
- public function provideCompletion(PhpDocument $document, Position $position): array
+ public function provideCompletion(PhpDocument $document, Position $pos): array
{
- $node = $document->getNodeAtPosition($position);
+ $node = $document->getNodeAtPosition($pos);
if ($node instanceof Node\Expr\Error) {
$node = $node->getAttribute('parentNode');
@@ -93,11 +166,12 @@ class CompletionProvider
}
}
} else if (
- // A ConstFetch means any static reference, like a class, interface, etc.
+ // A ConstFetch means any static reference, like a class, interface, etc. or keyword
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|| $node instanceof Node\Expr\New_
) {
- $prefix = null;
+ $prefix = '';
+ $prefixLen = 0;
if ($node instanceof Node\Name) {
$isFullyQualified = $node->isFullyQualified();
$prefix = (string)$node;
@@ -115,7 +189,7 @@ class CompletionProvider
foreach ($stmt->uses as $use) {
// Get the definition for the used namespace, class-like, function or constant
// And save it under the alias
- $fqn = (string)Node\Name::concat($stmt->prefix, $use->name);
+ $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
$aliasedDefs[$use->alias] = $this->project->getDefinition($fqn);
}
} else {
@@ -165,6 +239,16 @@ class CompletionProvider
$items[] = $item;
}
}
+ // Suggest keywords
+ if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
+ foreach (self::KEYWORDS as $keyword) {
+ if (substr($keyword, 0, $prefixLen) === $prefix) {
+ $item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
+ $item->insertText = $keyword . ' ';
+ $items[] = $item;
+ }
+ }
+ }
} else if (
$node instanceof Node\Expr\Variable
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
@@ -180,6 +264,13 @@ class CompletionProvider
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$items[] = $item;
}
+ } else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
+ $item = new CompletionItem('textEdit = new TextEdit(
+ new Range($pos, $pos),
+ stripStringOverlap($document->getRange(new Range(new Position(0, 0), $pos)), 'getAttribute('parentNode') instanceof Node\Stmt\Const_
);
+ $def->isStatic = (
+ ($node instanceof Node\Stmt\ClassMethod && $node->isStatic())
+ || ($node instanceof Node\Stmt\PropertyProperty && $node->getAttribute('parentNode')->isStatic())
+ );
$def->fqn = $fqn;
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
$def->type = $this->getTypeFromNode($node);
@@ -664,28 +668,35 @@ class DefinitionResolver
if ($node instanceof Node\Param) {
// Parameters
$docBlock = $node->getAttribute('parentNode')->getAttribute('docBlock');
- if (
- $docBlock !== null
- && !empty($paramTags = $docBlock->getTagsByName('param'))
- && $paramTags[0]->getType() !== null
- ) {
+ if ($docBlock !== null) {
// Use @param tag
- return $paramTags[0]->getType();
+ foreach ($docBlock->getTagsByName('param') as $paramTag) {
+ if ($paramTag->getVariableName() === $node->name) {
+ if ($paramTag->getType() === null) {
+ break;
+ }
+ return $paramTag->getType();
+ }
+ }
}
if ($node->type !== null) {
// Use PHP7 return type hint
if (is_string($node->type)) {
// Resolve a string like "bool" to a type object
$type = $this->typeResolver->resolve($node->type);
- }
- $type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
- if ($node->default !== null) {
- $defaultType = $this->resolveExpressionNodeToType($node->default);
- $type = new Types\Compound([$type, $defaultType]);
+ } else {
+ $type = new Types\Object_(new Fqsen('\\' . (string)$node->type));
}
}
- // Unknown parameter type
- return new Types\Mixed;
+ if ($node->default !== null) {
+ $defaultType = $this->resolveExpressionNodeToType($node->default);
+ if (isset($type) && !is_a($type, get_class($defaultType))) {
+ $type = new Types\Compound([$type, $defaultType]);
+ } else {
+ $type = $defaultType;
+ }
+ }
+ return $type ?? new Types\Mixed;
}
if ($node instanceof Node\FunctionLike) {
// Functions/methods
diff --git a/src/PhpDocument.php b/src/PhpDocument.php
index d37ec0a..cf16495 100644
--- a/src/PhpDocument.php
+++ b/src/PhpDocument.php
@@ -300,6 +300,22 @@ class PhpDocument
return $finder->node;
}
+ /**
+ * Returns a range of the content
+ *
+ * @param Range $range
+ * @return string|null
+ */
+ public function getRange(Range $range)
+ {
+ if ($this->content === null) {
+ return null;
+ }
+ $start = $range->start->toOffset($this->content);
+ $length = $range->end->toOffset($this->content) - $start;
+ return substr($this->content, $start, $length);
+ }
+
/**
* Returns the definition node for a fully qualified name
*
diff --git a/src/Protocol/CompletionItem.php b/src/Protocol/CompletionItem.php
index a764175..64bc69d 100644
--- a/src/Protocol/CompletionItem.php
+++ b/src/Protocol/CompletionItem.php
@@ -156,6 +156,9 @@ class CompletionItem
if ($def->documentation) {
$item->documentation = $def->documentation;
}
+ if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
+ $item->insertText = '$' . $def->symbolInformation->name;
+ }
return $item;
}
}
diff --git a/src/Protocol/Position.php b/src/Protocol/Position.php
index 01cff0b..1012fde 100644
--- a/src/Protocol/Position.php
+++ b/src/Protocol/Position.php
@@ -49,4 +49,16 @@ class Position
return $this->character - $position->character;
}
+
+ /**
+ * Returns the offset of the position in a string
+ *
+ * @param string $content
+ * @return int
+ */
+ public function toOffset(string $content): int
+ {
+ $lines = explode("\n", $content);
+ return array_sum(array_map('strlen', array_slice($lines, 0, $this->line))) + $this->character;
+ }
}
diff --git a/src/utils.php b/src/utils.php
index 172ccac..ed7a419 100644
--- a/src/utils.php
+++ b/src/utils.php
@@ -95,3 +95,25 @@ function getClosestNode(Node $node, string $type)
}
}
}
+
+/**
+ * Returns the part of $b that is not overlapped by $a
+ * Example:
+ *
+ * stripStringOverlap('whatever', 'body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message));
- } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) {
+ } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) {
$promise->fulfill();
}
}
@@ -109,7 +109,7 @@ class LanguageServerTest extends TestCase
if ($promise->state === Promise::PENDING) {
$promise->reject(new Exception($msg->body->params->message));
}
- } else if (strpos($msg->body->params->message, 'All 21 PHP files parsed') !== false) {
+ } else if (strpos($msg->body->params->message, 'All 24 PHP files parsed') !== false) {
// Indexing finished
$promise->fulfill();
}
diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php
index e5ab259..a7afb0d 100644
--- a/tests/Server/TextDocument/CompletionTest.php
+++ b/tests/Server/TextDocument/CompletionTest.php
@@ -5,8 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
-use LanguageServer\{Server, LanguageClient, Project};
-use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ClientCapabilities, CompletionItem, CompletionItemKind};
+use LanguageServer\{Server, LanguageClient, Project, CompletionProvider};
+use LanguageServer\Protocol\{
+ TextDocumentIdentifier,
+ TextEdit,
+ Range,
+ Position,
+ ClientCapabilities,
+ CompletionItem,
+ CompletionItemKind
+};
use function LanguageServer\pathToUri;
class CompletionTest extends TestCase
@@ -168,7 +176,10 @@ class CompletionTest extends TestCase
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
- 'Lorem excepteur officia sit anim velit veniam enim.'
+ 'Lorem excepteur officia sit anim velit veniam enim.',
+ null,
+ null,
+ '$staticTestProperty'
)
], $items);
}
@@ -192,7 +203,10 @@ class CompletionTest extends TestCase
'staticTestProperty',
CompletionItemKind::PROPERTY,
'\TestClass[]',
- 'Lorem excepteur officia sit anim velit veniam enim.'
+ 'Lorem excepteur officia sit anim velit veniam enim.',
+ null,
+ null,
+ '$staticTestProperty'
),
new CompletionItem(
'staticTestMethod',
@@ -259,4 +273,62 @@ class CompletionTest extends TestCase
)
], $items);
}
+
+ public function testKeywords()
+ {
+ $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php');
+ $this->project->openDocument($completionUri, file_get_contents($completionUri));
+ $items = $this->textDocument->completion(
+ new TextDocumentIdentifier($completionUri),
+ new Position(2, 1)
+ )->wait();
+ $this->assertEquals([
+ new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '),
+ new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ')
+ ], $items);
+ }
+
+ public function testHtmlWithoutPrefix()
+ {
+ $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php');
+ $this->project->openDocument($completionUri, file_get_contents($completionUri));
+ $items = $this->textDocument->completion(
+ new TextDocumentIdentifier($completionUri),
+ new Position(0, 0)
+ )->wait();
+ $this->assertEquals([
+ new CompletionItem(
+ 'project->openDocument($completionUri, file_get_contents($completionUri));
+ $items = $this->textDocument->completion(
+ new TextDocumentIdentifier($completionUri),
+ new Position(0, 1)
+ )->wait();
+ $this->assertEquals([
+ new CompletionItem(
+ 'assertEquals('assertEquals('?php', stripStringOverlap('bla<', 'assertEquals('php', stripStringOverlap('bla', 'assertEquals('', stripStringOverlap('blaassertEquals('assertEquals('', stripStringOverlap('bla', ''));
+ }
+
+ public function testBothEmpty()
+ {
+ $this->assertEquals('', stripStringOverlap('', ''));
+ }
+}