commit
7032f806d4
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TestNamespace;
|
||||||
|
|
||||||
|
$obj = new TestClass();
|
||||||
|
$obj->testMethod();
|
||||||
|
echo $obj->testProperty;
|
||||||
|
TestClass::staticTestMethod();
|
||||||
|
echo TestClass::$staticTestProperty;
|
||||||
|
echo TestClass::TEST_CLASS_CONST;
|
||||||
|
test_function();
|
||||||
|
|
||||||
|
$var = 123;
|
||||||
|
echo $var;
|
||||||
|
|
||||||
|
function whatever(TestClass $param): TestClass {
|
||||||
|
echo $param;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fn = function() use ($var) {
|
||||||
|
echo $var;
|
||||||
|
};
|
||||||
|
|
||||||
|
echo TEST_CONST;
|
|
@ -2,10 +2,19 @@
|
||||||
|
|
||||||
namespace TestNamespace;
|
namespace TestNamespace;
|
||||||
|
|
||||||
class TestClass
|
const TEST_CONST = 123;
|
||||||
|
|
||||||
|
class TestClass implements TestInterface
|
||||||
{
|
{
|
||||||
|
const TEST_CLASS_CONST = 123;
|
||||||
|
public static $staticTestProperty;
|
||||||
public $testProperty;
|
public $testProperty;
|
||||||
|
|
||||||
|
public static function staticTestMethod()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function testMethod($testParameter)
|
public function testMethod($testParameter)
|
||||||
{
|
{
|
||||||
$testVariable = 123;
|
$testVariable = 123;
|
||||||
|
@ -21,3 +30,24 @@ interface TestInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_function()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
new class {
|
||||||
|
const TEST_CLASS_CONST = 123;
|
||||||
|
public static $staticTestProperty;
|
||||||
|
public $testProperty;
|
||||||
|
|
||||||
|
public static function staticTestMethod()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMethod($testParameter)
|
||||||
|
{
|
||||||
|
$testVariable = 123;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SecondTestNamespace;
|
||||||
|
|
||||||
|
use TestNamespace\TestClass;
|
||||||
|
use TestNamespace\{TestTrait, TestInterface};
|
|
@ -103,6 +103,9 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
$serverCapabilities->workspaceSymbolProvider = true;
|
$serverCapabilities->workspaceSymbolProvider = true;
|
||||||
// Support "Format Code"
|
// Support "Format Code"
|
||||||
$serverCapabilities->documentFormattingProvider = true;
|
$serverCapabilities->documentFormattingProvider = true;
|
||||||
|
// Support "Go to definition"
|
||||||
|
$serverCapabilities->definitionProvider = true;
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types = 1);
|
declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
use PhpParser\{NodeVisitorAbstract, Node};
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects definitions of classes, interfaces, traits, methods, properties and constants
|
||||||
|
* Depends on ReferencesAdder and NameResolver
|
||||||
|
*/
|
||||||
|
class DefinitionCollector extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Map from fully qualified name (FQN) to Node
|
||||||
|
*
|
||||||
|
* @var Node[]
|
||||||
|
*/
|
||||||
|
public $definitions = [];
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
$fqn = $node->getAttribute('ownerDocument')->getDefinedFqn($node);
|
||||||
|
if ($fqn !== null) {
|
||||||
|
$this->definitions[$fqn] = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
use LanguageServer\Protocol\{Position, Range};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the Node at a specified position
|
||||||
|
* Depends on ColumnCalculator
|
||||||
|
*/
|
||||||
|
class NodeAtPositionFinder extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The node at the position, if found
|
||||||
|
*
|
||||||
|
* @var Node
|
||||||
|
*/
|
||||||
|
public $node;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Position
|
||||||
|
*/
|
||||||
|
private $position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Position $position The position where the node is located
|
||||||
|
*/
|
||||||
|
public function __construct(Position $position)
|
||||||
|
{
|
||||||
|
$this->position = $position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveNode(Node $node)
|
||||||
|
{
|
||||||
|
$range = Range::fromNode($node);
|
||||||
|
// Workaround for https://github.com/nikic/PHP-Parser/issues/311
|
||||||
|
$parent = $node->getAttribute('parentNode');
|
||||||
|
if (isset($parent) && $parent instanceof Node\Stmt\GroupUse && $parent->prefix === $node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($this->node) && $range->includes($this->position)) {
|
||||||
|
$this->node = $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\NodeVisitor;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
use LanguageServer\Protocol\{SymbolInformation, SymbolKind, Range, Position, Location};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorates all nodes with parent and sibling references (similar to DOM nodes)
|
||||||
|
*/
|
||||||
|
class ReferencesAdder extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Node[]
|
||||||
|
*/
|
||||||
|
private $stack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Node
|
||||||
|
*/
|
||||||
|
private $previous;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
private $document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $document The value for the ownerDocument attribute
|
||||||
|
*/
|
||||||
|
public function __construct($document = null)
|
||||||
|
{
|
||||||
|
$this->document = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
$node->setAttribute('ownerDocument', $this->document);
|
||||||
|
if (!empty($this->stack)) {
|
||||||
|
$node->setAttribute('parentNode', end($this->stack));
|
||||||
|
}
|
||||||
|
if (isset($this->previous) && $this->previous->getAttribute('parentNode') === $node->getAttribute('parentNode')) {
|
||||||
|
$node->setAttribute('previousSibling', $this->previous);
|
||||||
|
$this->previous->setAttribute('nextSibling', $node);
|
||||||
|
}
|
||||||
|
$this->stack[] = $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveNode(Node $node)
|
||||||
|
{
|
||||||
|
$this->previous = $node;
|
||||||
|
array_pop($this->stack);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use \LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolKind, TextEdit};
|
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, SymbolInformation, SymbolKind, TextEdit, Location};
|
||||||
|
use LanguageServer\NodeVisitor\{NodeAtPositionFinder, ReferencesAdder, DefinitionCollector, ColumnCalculator};
|
||||||
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer, Parser};
|
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer, Parser};
|
||||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
@ -23,7 +23,9 @@ class PhpDocument
|
||||||
*
|
*
|
||||||
* @var Project
|
* @var Project
|
||||||
*/
|
*/
|
||||||
private $project;
|
public $project;
|
||||||
|
// for whatever reason I get "cannot access private property" error if $project is not public
|
||||||
|
// https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The PHPParser instance
|
* The PHPParser instance
|
||||||
|
@ -47,9 +49,25 @@ class PhpDocument
|
||||||
private $content;
|
private $content;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var SymbolInformation[]
|
* The AST of the document
|
||||||
|
*
|
||||||
|
* @var Node[]
|
||||||
*/
|
*/
|
||||||
private $symbols = [];
|
private $stmts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from fully qualified name (FQN) to Node
|
||||||
|
*
|
||||||
|
* @var Node[]
|
||||||
|
*/
|
||||||
|
private $definitions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
|
||||||
|
*
|
||||||
|
* @var Node[][]
|
||||||
|
*/
|
||||||
|
private $references;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
|
@ -68,22 +86,54 @@ class PhpDocument
|
||||||
/**
|
/**
|
||||||
* Returns all symbols in this document.
|
* Returns all symbols in this document.
|
||||||
*
|
*
|
||||||
* @return SymbolInformation[]
|
* @return SymbolInformation[]|null
|
||||||
*/
|
*/
|
||||||
public function getSymbols()
|
public function getSymbols()
|
||||||
{
|
{
|
||||||
return $this->symbols;
|
if (!isset($this->definitions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$nodeSymbolKindMap = [
|
||||||
|
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
||||||
|
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
|
||||||
|
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
||||||
|
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
||||||
|
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
||||||
|
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
|
||||||
|
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
|
||||||
|
Node\Const_::class => SymbolKind::CONSTANT
|
||||||
|
];
|
||||||
|
$symbols = [];
|
||||||
|
foreach ($this->definitions as $fqn => $node) {
|
||||||
|
$class = get_class($node);
|
||||||
|
if (!isset($nodeSymbolKindMap[$class])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$symbol = new SymbolInformation();
|
||||||
|
$symbol->kind = $nodeSymbolKindMap[$class];
|
||||||
|
$symbol->name = (string)$node->name;
|
||||||
|
$symbol->location = Location::fromNode($node);
|
||||||
|
$parts = preg_split('/(::|\\\\)/', $fqn);
|
||||||
|
array_pop($parts);
|
||||||
|
$symbol->containerName = implode('\\', $parts);
|
||||||
|
$symbols[] = $symbol;
|
||||||
|
}
|
||||||
|
return $symbols;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns symbols in this document filtered by query string.
|
* Returns symbols in this document filtered by query string.
|
||||||
*
|
*
|
||||||
* @param string $query The search query
|
* @param string $query The search query
|
||||||
* @return SymbolInformation[]
|
* @return SymbolInformation[]|null
|
||||||
*/
|
*/
|
||||||
public function findSymbols(string $query)
|
public function findSymbols(string $query)
|
||||||
{
|
{
|
||||||
return array_filter($this->symbols, function($symbol) use(&$query) {
|
$symbols = $this->getSymbols();
|
||||||
|
if ($symbols === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return array_filter($symbols, function($symbol) use ($query) {
|
||||||
return stripos($symbol->name, $query) !== false;
|
return stripos($symbol->name, $query) !== false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -104,7 +154,7 @@ class PhpDocument
|
||||||
* Re-parses a source file, updates symbols, reports parsing errors
|
* Re-parses a source file, updates symbols, reports parsing errors
|
||||||
* that may have occured as diagnostics and returns parsed nodes.
|
* that may have occured as diagnostics and returns parsed nodes.
|
||||||
*
|
*
|
||||||
* @return \PhpParser\Node[]
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function parse()
|
public function parse()
|
||||||
{
|
{
|
||||||
|
@ -138,16 +188,30 @@ class PhpDocument
|
||||||
// $stmts can be null in case of a fatal parsing error
|
// $stmts can be null in case of a fatal parsing error
|
||||||
if ($stmts) {
|
if ($stmts) {
|
||||||
$traverser = new NodeTraverser;
|
$traverser = new NodeTraverser;
|
||||||
$finder = new SymbolFinder($this->uri);
|
|
||||||
|
// Resolve aliased names to FQNs
|
||||||
$traverser->addVisitor(new NameResolver);
|
$traverser->addVisitor(new NameResolver);
|
||||||
|
|
||||||
|
// Add parentNode, previousSibling, nextSibling attributes
|
||||||
|
$traverser->addVisitor(new ReferencesAdder($this));
|
||||||
|
|
||||||
|
// Add column attributes to nodes
|
||||||
$traverser->addVisitor(new ColumnCalculator($this->content));
|
$traverser->addVisitor(new ColumnCalculator($this->content));
|
||||||
$traverser->addVisitor($finder);
|
|
||||||
|
// Collect all definitions
|
||||||
|
$definitionCollector = new DefinitionCollector;
|
||||||
|
$traverser->addVisitor($definitionCollector);
|
||||||
|
|
||||||
$traverser->traverse($stmts);
|
$traverser->traverse($stmts);
|
||||||
|
|
||||||
$this->symbols = $finder->symbols;
|
$this->definitions = $definitionCollector->definitions;
|
||||||
}
|
// Register this document on the project for all the symbols defined in it
|
||||||
|
foreach ($definitionCollector->definitions as $fqn => $node) {
|
||||||
|
$this->project->addDefinitionDocument($fqn, $this);
|
||||||
|
}
|
||||||
|
|
||||||
return $stmts;
|
$this->stmts = $stmts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,14 +221,13 @@ class PhpDocument
|
||||||
*/
|
*/
|
||||||
public function getFormattedText()
|
public function getFormattedText()
|
||||||
{
|
{
|
||||||
$stmts = $this->parse();
|
if (empty($this->stmts)) {
|
||||||
if (empty($stmts)) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$prettyPrinter = new PrettyPrinter();
|
$prettyPrinter = new PrettyPrinter();
|
||||||
$edit = new TextEdit();
|
$edit = new TextEdit();
|
||||||
$edit->range = new Range(new Position(0, 0), new Position(PHP_INT_MAX, PHP_INT_MAX));
|
$edit->range = new Range(new Position(0, 0), new Position(PHP_INT_MAX, PHP_INT_MAX));
|
||||||
$edit->newText = $prettyPrinter->prettyPrintFile($stmts);
|
$edit->newText = $prettyPrinter->prettyPrintFile($this->stmts);
|
||||||
return [$edit];
|
return [$edit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,4 +240,294 @@ class PhpDocument
|
||||||
{
|
{
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URI of the document
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUri(): string
|
||||||
|
{
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the node at a specified position
|
||||||
|
*
|
||||||
|
* @param Position $position
|
||||||
|
* @return Node|null
|
||||||
|
*/
|
||||||
|
public function getNodeAtPosition(Position $position)
|
||||||
|
{
|
||||||
|
if ($this->stmts === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$traverser = new NodeTraverser;
|
||||||
|
$finder = new NodeAtPositionFinder($position);
|
||||||
|
$traverser->addVisitor($finder);
|
||||||
|
$traverser->traverse($this->stmts);
|
||||||
|
return $finder->node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the definition node for a fully qualified name
|
||||||
|
*
|
||||||
|
* @param string $fqn
|
||||||
|
* @return Node|null
|
||||||
|
*/
|
||||||
|
public function getDefinitionByFqn(string $fqn)
|
||||||
|
{
|
||||||
|
return $this->definitions[$fqn] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given FQN is defined in this document
|
||||||
|
*
|
||||||
|
* @param string $fqn The fully qualified name of the symbol
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDefined(string $fqn): bool
|
||||||
|
{
|
||||||
|
return isset($this->definitions[$fqn]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fully qualified name (FQN) that is defined by a node
|
||||||
|
* Examples of FQNs:
|
||||||
|
* - testFunction()
|
||||||
|
* - TestNamespace\TestClass
|
||||||
|
* - TestNamespace\TestClass::TEST_CONSTANT
|
||||||
|
* - TestNamespace\TestClass::staticTestProperty
|
||||||
|
* - TestNamespace\TestClass::testProperty
|
||||||
|
* - TestNamespace\TestClass::staticTestMethod()
|
||||||
|
* - TestNamespace\TestClass::testMethod()
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getDefinedFqn(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Name) {
|
||||||
|
$nameNode = $node;
|
||||||
|
$node = $node->getAttribute('parentNode');
|
||||||
|
}
|
||||||
|
// Only the class node should count as the definition, not the name node
|
||||||
|
// Anonymous classes don't count as a definition
|
||||||
|
if ($node instanceof Node\Stmt\ClassLike && !isset($nameNode) && isset($node->name)) {
|
||||||
|
// Class, interface or trait declaration
|
||||||
|
return (string)$node->namespacedName;
|
||||||
|
} else if ($node instanceof Node\Stmt\Function_) {
|
||||||
|
// Function: use functionName() as the name
|
||||||
|
return (string)$node->namespacedName . '()';
|
||||||
|
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
||||||
|
// Class method: use ClassName::methodName() as name
|
||||||
|
$class = $node->getAttribute('parentNode');
|
||||||
|
if (!isset($class->name)) {
|
||||||
|
// Ignore anonymous classes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (string)$class->namespacedName . '::' . (string)$node->name . '()';
|
||||||
|
} else if ($node instanceof Node\Stmt\PropertyProperty) {
|
||||||
|
// Property: use ClassName::propertyName as name
|
||||||
|
$class = $node->getAttribute('parentNode')->getAttribute('parentNode');
|
||||||
|
if (!isset($class->name)) {
|
||||||
|
// Ignore anonymous classes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (string)$class->namespacedName . '::' . (string)$node->name;
|
||||||
|
} else if ($node instanceof Node\Const_) {
|
||||||
|
$parent = $node->getAttribute('parentNode');
|
||||||
|
if ($parent instanceof Node\Stmt\Const_) {
|
||||||
|
// Basic constant: use CONSTANT_NAME as name
|
||||||
|
return (string)$node->namespacedName;
|
||||||
|
}
|
||||||
|
if ($parent instanceof Node\Stmt\ClassConst) {
|
||||||
|
// Class constant: use ClassName::CONSTANT_NAME as name
|
||||||
|
$class = $parent->getAttribute('parentNode');
|
||||||
|
if (!isset($class->name) || $class->name instanceof Node\Expr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (string)$class->namespacedName . '::' . $node->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the FQN that is referenced by a node
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getReferencedFqn(Node $node)
|
||||||
|
{
|
||||||
|
if ($node instanceof Node\Name) {
|
||||||
|
$nameNode = $node;
|
||||||
|
$node = $node->getAttribute('parentNode');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
($node instanceof Node\Stmt\ClassLike
|
||||||
|
|| $node instanceof Node\Param
|
||||||
|
|| $node instanceof Node\Stmt\Function_)
|
||||||
|
&& isset($nameNode)
|
||||||
|
) {
|
||||||
|
// For extends, implements and type hints use the name directly
|
||||||
|
$name = (string)$nameNode;
|
||||||
|
// Only the name node should be considered a reference, not the UseUse node itself
|
||||||
|
} else if ($node instanceof Node\Stmt\UseUse && isset($nameNode)) {
|
||||||
|
$name = (string)$node->name;
|
||||||
|
$parent = $node->getAttribute('parentNode');
|
||||||
|
if ($parent instanceof Node\Stmt\GroupUse) {
|
||||||
|
$name = $parent->prefix . '\\' . $name;
|
||||||
|
}
|
||||||
|
// Only the name node should be considered a reference, not the New_ node itself
|
||||||
|
} else if ($node instanceof Node\Expr\New_ && isset($nameNode)) {
|
||||||
|
if (!($node->class instanceof Node\Name)) {
|
||||||
|
// Cannot get definition of dynamic calls
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = (string)$node->class;
|
||||||
|
} else if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
|
||||||
|
if ($node->name instanceof Node\Expr || !($node->var instanceof Node\Expr\Variable)) {
|
||||||
|
// Cannot get definition of dynamic calls
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Need to resolve variable to a class
|
||||||
|
$varDef = $this->getVariableDefinition($node->var);
|
||||||
|
if (!isset($varDef)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($varDef instanceof Node\Param) {
|
||||||
|
if (!isset($varDef->type)) {
|
||||||
|
// Cannot resolve to class without a type hint
|
||||||
|
// TODO: parse docblock
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = (string)$varDef->type;
|
||||||
|
} else if ($varDef instanceof Node\Expr\Assign) {
|
||||||
|
if ($varDef->expr instanceof Node\Expr\New_) {
|
||||||
|
if (!($varDef->expr->class instanceof Node\Name)) {
|
||||||
|
// Cannot get definition of dynamic calls
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = (string)$varDef->expr->class;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name .= '::' . (string)$node->name;
|
||||||
|
} else if ($node instanceof Node\Expr\FuncCall) {
|
||||||
|
if ($node->name instanceof Node\Expr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = (string)$node->name;
|
||||||
|
} else if ($node instanceof Node\Expr\ConstFetch) {
|
||||||
|
$name = (string)$node->name;
|
||||||
|
} else if (
|
||||||
|
$node instanceof Node\Expr\ClassConstFetch
|
||||||
|
|| $node instanceof Node\Expr\StaticPropertyFetch
|
||||||
|
|| $node instanceof Node\Expr\StaticCall
|
||||||
|
) {
|
||||||
|
if ($node->class instanceof Node\Expr || $node->name instanceof Node\Expr) {
|
||||||
|
// Cannot get definition of dynamic names
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$name = (string)$node->class . '::' . $node->name;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$node instanceof Node\Expr\MethodCall
|
||||||
|
|| $node instanceof Node\Expr\FuncCall
|
||||||
|
|| $node instanceof Node\Expr\StaticCall
|
||||||
|
) {
|
||||||
|
$name .= '()';
|
||||||
|
}
|
||||||
|
if (!isset($name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
|
||||||
|
// The NameResolver therefor does not resolve these to namespaced names
|
||||||
|
// http://php.net/manual/en/language.namespaces.fallback.php
|
||||||
|
if ($node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\ConstFetch) {
|
||||||
|
// Find and try with namespace
|
||||||
|
$n = $node;
|
||||||
|
while (isset($n)) {
|
||||||
|
$n = $n->getAttribute('parentNode');
|
||||||
|
if ($n instanceof Node\Stmt\Namespace_) {
|
||||||
|
$namespacedName = (string)$n->name . '\\' . $name;
|
||||||
|
// If the namespaced version is defined, return that
|
||||||
|
// Otherwise fall back to global
|
||||||
|
if ($this->project->isDefined($namespacedName)) {
|
||||||
|
return $namespacedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the definition node for any node
|
||||||
|
* The definition node MAY be in another document, check the ownerDocument attribute
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return Node|null
|
||||||
|
*/
|
||||||
|
public function getDefinitionByNode(Node $node)
|
||||||
|
{
|
||||||
|
// Variables always stay in the boundary of the file and need to be searched inside their function scope
|
||||||
|
// by traversing the AST
|
||||||
|
if ($node instanceof Node\Expr\Variable) {
|
||||||
|
return $this->getVariableDefinition($node);
|
||||||
|
}
|
||||||
|
$fqn = $this->getReferencedFqn($node);
|
||||||
|
if (!isset($fqn)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$document = $this->project->getDefinitionDocument($fqn);
|
||||||
|
if (!isset($document)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $document->getDefinitionByFqn($fqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the assignment or parameter node where a variable was defined
|
||||||
|
*
|
||||||
|
* @param Node\Expr\Variable $n The variable access
|
||||||
|
* @return Node\Expr\Assign|Node\Param|Node\Expr\ClosureUse|null
|
||||||
|
*/
|
||||||
|
public function getVariableDefinition(Node\Expr\Variable $var)
|
||||||
|
{
|
||||||
|
$n = $var;
|
||||||
|
// Traverse the AST up
|
||||||
|
while (isset($n) && $n = $n->getAttribute('parentNode')) {
|
||||||
|
// If a function is met, check the parameters and use statements
|
||||||
|
if ($n instanceof Node\FunctionLike) {
|
||||||
|
foreach ($n->getParams() as $param) {
|
||||||
|
if ($param->name === $var->name) {
|
||||||
|
return $param;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it is a closure, also check use statements
|
||||||
|
if ($n instanceof Node\Expr\Closure) {
|
||||||
|
foreach ($n->uses as $use) {
|
||||||
|
if ($use->var === $var->name) {
|
||||||
|
return $use;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Check each previous sibling node for a variable assignment to that variable
|
||||||
|
while ($n->getAttribute('previousSibling') && $n = $n->getAttribute('previousSibling')) {
|
||||||
|
if ($n instanceof Node\Expr\Assign && $n->var->name === $var->name) {
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return null if nothing was found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,15 @@ class Project
|
||||||
*
|
*
|
||||||
* @var PhpDocument[]
|
* @var PhpDocument[]
|
||||||
*/
|
*/
|
||||||
private $documents;
|
private $documents = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An associative array [string => PhpDocument]
|
||||||
|
* that maps fully qualified symbol names to loaded PhpDocuments
|
||||||
|
*
|
||||||
|
* @var PhpDocument[]
|
||||||
|
*/
|
||||||
|
private $definitions = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance of the PHP parser
|
* Instance of the PHP parser
|
||||||
|
@ -54,6 +62,39 @@ class Project
|
||||||
return $this->documents[$uri];
|
return $this->documents[$uri];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a document as the container for a specific symbol
|
||||||
|
*
|
||||||
|
* @param string $fqn The fully qualified name of the symbol
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addDefinitionDocument(string $fqn, PhpDocument $document)
|
||||||
|
{
|
||||||
|
$this->definitions[$fqn] = $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the document where a symbol is defined
|
||||||
|
*
|
||||||
|
* @param string $fqn The fully qualified name of the symbol
|
||||||
|
* @return PhpDocument|null
|
||||||
|
*/
|
||||||
|
public function getDefinitionDocument(string $fqn)
|
||||||
|
{
|
||||||
|
return $this->definitions[$fqn] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given FQN is defined in the project
|
||||||
|
*
|
||||||
|
* @param string $fqn The fully qualified name of the symbol
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isDefined(string $fqn): bool
|
||||||
|
{
|
||||||
|
return isset($this->definitions[$fqn]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds symbols in all documents, filtered by query parameter.
|
* Finds symbols in all documents, filtered by query parameter.
|
||||||
*
|
*
|
||||||
|
@ -64,7 +105,10 @@ class Project
|
||||||
{
|
{
|
||||||
$queryResult = [];
|
$queryResult = [];
|
||||||
foreach ($this->documents as $uri => $document) {
|
foreach ($this->documents as $uri => $document) {
|
||||||
$queryResult = array_merge($queryResult, $document->findSymbols($query));
|
$documentQueryResult = $document->findSymbols($query);
|
||||||
|
if ($documentQueryResult !== null) {
|
||||||
|
$queryResult = array_merge($queryResult, $documentQueryResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $queryResult;
|
return $queryResult;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a location inside a resource, such as a line inside a text file.
|
* Represents a location inside a resource, such as a line inside a text file.
|
||||||
*/
|
*/
|
||||||
|
@ -17,6 +19,17 @@ class Location
|
||||||
*/
|
*/
|
||||||
public $range;
|
public $range;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the location of the node
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromNode(Node $node)
|
||||||
|
{
|
||||||
|
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node));
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(string $uri = null, Range $range = null)
|
public function __construct(string $uri = null, Range $range = null)
|
||||||
{
|
{
|
||||||
$this->uri = $uri;
|
$this->uri = $uri;
|
||||||
|
|
|
@ -26,4 +26,27 @@ class Position
|
||||||
$this->line = $line;
|
$this->line = $line;
|
||||||
$this->character = $character;
|
$this->character = $character;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this position to another position
|
||||||
|
* Returns
|
||||||
|
* - 0 if the positions match
|
||||||
|
* - a negative number if $this is before $position
|
||||||
|
* - a positive number otherwise
|
||||||
|
*
|
||||||
|
* @param Position $position
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function compare(Position $position): int
|
||||||
|
{
|
||||||
|
if ($this->line === $position->line && $this->character === $position->character) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->line !== $position->line) {
|
||||||
|
return $this->line - $position->line;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->character - $position->character;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A range in a text document expressed as (zero-based) start and end positions.
|
* A range in a text document expressed as (zero-based) start and end positions.
|
||||||
*/
|
*/
|
||||||
|
@ -21,9 +23,34 @@ class Range
|
||||||
*/
|
*/
|
||||||
public $end;
|
public $end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the range the node spans
|
||||||
|
*
|
||||||
|
* @param Node $node
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromNode(Node $node)
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
|
||||||
|
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(Position $start = null, Position $end = null)
|
public function __construct(Position $start = null, Position $end = null)
|
||||||
{
|
{
|
||||||
$this->start = $start;
|
$this->start = $start;
|
||||||
$this->end = $end;
|
$this->end = $end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a position is within the range
|
||||||
|
*
|
||||||
|
* @param Position $position
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function includes(Position $position): bool
|
||||||
|
{
|
||||||
|
return $this->start->compare($position) <= 0 && $this->end->compare($position) >= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents information about programming constructs like variables, classes,
|
* Represents information about programming constructs like variables, classes,
|
||||||
* interfaces etc.
|
* interfaces etc.
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder, Project};
|
use LanguageServer\{LanguageClient, ColumnCalculator, Project};
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
TextDocumentItem,
|
TextDocumentItem,
|
||||||
TextDocumentIdentifier,
|
TextDocumentIdentifier,
|
||||||
|
@ -13,7 +13,8 @@ use LanguageServer\Protocol\{
|
||||||
Range,
|
Range,
|
||||||
Position,
|
Position,
|
||||||
FormattingOptions,
|
FormattingOptions,
|
||||||
TextEdit
|
TextEdit,
|
||||||
|
Location
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,4 +89,26 @@ class TextDocument
|
||||||
{
|
{
|
||||||
return $this->project->getDocument($textDocument->uri)->getFormattedText();
|
return $this->project->getDocument($textDocument->uri)->getFormattedText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Location|Location[]|null
|
||||||
|
*/
|
||||||
|
public function definition(TextDocumentIdentifier $textDocument, Position $position)
|
||||||
|
{
|
||||||
|
$document = $this->project->getDocument($textDocument->uri);
|
||||||
|
$node = $document->getNodeAtPosition($position);
|
||||||
|
if ($node === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$def = $document->getDefinitionByNode($node);
|
||||||
|
if ($def === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Location::fromNode($def);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace LanguageServer\Server;
|
||||||
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||||
use PhpParser\NodeVisitor\NameResolver;
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder, Project};
|
use LanguageServer\{LanguageClient, ColumnCalculator, Project};
|
||||||
use LanguageServer\Protocol\{
|
use LanguageServer\Protocol\{
|
||||||
TextDocumentItem,
|
TextDocumentItem,
|
||||||
TextDocumentIdentifier,
|
TextDocumentIdentifier,
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
<?php
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace LanguageServer;
|
|
||||||
|
|
||||||
use PhpParser\{NodeVisitorAbstract, Node};
|
|
||||||
|
|
||||||
use LanguageServer\Protocol\{SymbolInformation, SymbolKind, Range, Position, Location};
|
|
||||||
|
|
||||||
class SymbolFinder extends NodeVisitorAbstract
|
|
||||||
{
|
|
||||||
const NODE_SYMBOL_KIND_MAP = [
|
|
||||||
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
|
||||||
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
|
|
||||||
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
|
||||||
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
|
||||||
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
|
||||||
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
|
|
||||||
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
|
|
||||||
Node\Const_::class => SymbolKind::CONSTANT,
|
|
||||||
Node\Expr\Variable::class => SymbolKind::VARIABLE
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LanguageServer\Protocol\SymbolInformation[]
|
|
||||||
*/
|
|
||||||
public $symbols = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $containerName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $nameStack = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $nodeStack = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private $functionCount = 0;
|
|
||||||
|
|
||||||
public function __construct(string $uri)
|
|
||||||
{
|
|
||||||
$this->uri = $uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function enterNode(Node $node)
|
|
||||||
{
|
|
||||||
$this->nodeStack[] = $node;
|
|
||||||
$containerName = end($this->nameStack);
|
|
||||||
|
|
||||||
// If we enter a named node, push its name onto name stack.
|
|
||||||
// Else push the current name onto stack.
|
|
||||||
if (!empty($node->name) && (is_string($node->name) || method_exists($node->name, '__toString')) && !empty((string)$node->name)) {
|
|
||||||
if (empty($containerName)) {
|
|
||||||
$this->nameStack[] = (string)$node->name;
|
|
||||||
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
|
||||||
$this->nameStack[] = $containerName . '::' . (string)$node->name;
|
|
||||||
} else {
|
|
||||||
$this->nameStack[] = $containerName . '\\' . (string)$node->name;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->nameStack[] = $containerName;
|
|
||||||
// We are not interested in unnamed nodes, return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$class = get_class($node);
|
|
||||||
if (!isset(self::NODE_SYMBOL_KIND_MAP[$class])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we enter a method or function, increase the function counter
|
|
||||||
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
|
|
||||||
$this->functionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$kind = self::NODE_SYMBOL_KIND_MAP[$class];
|
|
||||||
|
|
||||||
// exclude non-global variable symbols.
|
|
||||||
if ($kind === SymbolKind::VARIABLE && $this->functionCount > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$symbol = new SymbolInformation();
|
|
||||||
$symbol->kind = $kind;
|
|
||||||
$symbol->name = (string)$node->name;
|
|
||||||
$symbol->location = new Location(
|
|
||||||
$this->uri,
|
|
||||||
new Range(
|
|
||||||
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
|
|
||||||
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$symbol->containerName = $containerName;
|
|
||||||
$this->symbols[] = $symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function leaveNode(Node $node)
|
|
||||||
{
|
|
||||||
array_pop($this->nodeStack);
|
|
||||||
array_pop($this->nameStack);
|
|
||||||
|
|
||||||
// if we leave a method or function, decrease the function counter
|
|
||||||
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
|
|
||||||
$this->functionCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,7 +34,7 @@ class LanguageServerTest extends TestCase
|
||||||
'hoverProvider' => null,
|
'hoverProvider' => null,
|
||||||
'completionProvider' => null,
|
'completionProvider' => null,
|
||||||
'signatureHelpProvider' => null,
|
'signatureHelpProvider' => null,
|
||||||
'definitionProvider' => null,
|
'definitionProvider' => true,
|
||||||
'referencesProvider' => null,
|
'referencesProvider' => null,
|
||||||
'documentHighlightProvider' => null,
|
'documentHighlightProvider' => null,
|
||||||
'workspaceSymbolProvider' => true,
|
'workspaceSymbolProvider' => true,
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PhpParser\{ParserFactory, NodeTraverser, Node};
|
||||||
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
use LanguageServer\{LanguageClient, Project, PhpDocument};
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
|
||||||
|
|
||||||
|
class DefinitionCollectorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
||||||
|
$document = new PhpDocument('whatever', $project, $client, $parser);
|
||||||
|
$traverser = new NodeTraverser;
|
||||||
|
$traverser->addVisitor(new NameResolver);
|
||||||
|
$traverser->addVisitor(new ReferencesAdder($document));
|
||||||
|
$definitionCollector = new DefinitionCollector;
|
||||||
|
$traverser->addVisitor($definitionCollector);
|
||||||
|
$stmts = $parser->parse(file_get_contents(__DIR__ . '/../../fixtures/symbols.php'));
|
||||||
|
$traverser->traverse($stmts);
|
||||||
|
$defs = $definitionCollector->definitions;
|
||||||
|
$this->assertEquals([
|
||||||
|
'TestNamespace\\TEST_CONST',
|
||||||
|
'TestNamespace\\TestClass',
|
||||||
|
'TestNamespace\\TestClass::TEST_CLASS_CONST',
|
||||||
|
'TestNamespace\\TestClass::staticTestProperty',
|
||||||
|
'TestNamespace\\TestClass::testProperty',
|
||||||
|
'TestNamespace\\TestClass::staticTestMethod()',
|
||||||
|
'TestNamespace\\TestClass::testMethod()',
|
||||||
|
'TestNamespace\\TestTrait',
|
||||||
|
'TestNamespace\\TestInterface',
|
||||||
|
'TestNamespace\\test_function()'
|
||||||
|
], array_keys($defs));
|
||||||
|
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TEST_CONST']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\Class_::class, $defs['TestNamespace\\TestClass']);
|
||||||
|
$this->assertInstanceOf(Node\Const_::class, $defs['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::staticTestProperty']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defs['TestNamespace\\TestClass::testProperty']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::staticTestMethod()']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defs['TestNamespace\\TestClass::testMethod()']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defs['TestNamespace\\TestTrait']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defs['TestNamespace\\TestInterface']);
|
||||||
|
$this->assertInstanceOf(Node\Stmt\Function_::class, $defs['TestNamespace\\test_function()']);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,9 @@ namespace LanguageServer\Tests\Server;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use LanguageServer\Tests\MockProtocolStream;
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
use LanguageServer\{LanguageClient, Project};
|
use LanguageServer\{LanguageClient, Project};
|
||||||
use LanguageServer\Protocol\SymbolKind;
|
use LanguageServer\NodeVisitor\NodeAtPositionFinder;
|
||||||
|
use LanguageServer\Protocol\{SymbolKind, Position};
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
class PhpDocumentTest extends TestCase
|
class PhpDocumentTest extends TestCase
|
||||||
{
|
{
|
||||||
|
@ -28,43 +30,15 @@ class PhpDocumentTest extends TestCase
|
||||||
|
|
||||||
$symbols = $document->getSymbols();
|
$symbols = $document->getSymbols();
|
||||||
|
|
||||||
$this->assertEquals([
|
$this->assertEquals([], json_decode(json_encode($symbols), true));
|
||||||
[
|
}
|
||||||
'name' => 'a',
|
|
||||||
'kind' => SymbolKind::VARIABLE,
|
public function testGetNodeAtPosition()
|
||||||
'location' => [
|
{
|
||||||
'uri' => 'whatever',
|
$document = $this->project->getDocument('whatever');
|
||||||
'range' => [
|
$document->updateContent("<?php\n$\$a = new SomeClass;");
|
||||||
'start' => [
|
$node = $document->getNodeAtPosition(new Position(1, 13));
|
||||||
'line' => 1,
|
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
|
||||||
'character' => 0
|
$this->assertEquals('SomeClass', (string)$node);
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 1,
|
|
||||||
'character' => 3
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => null
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'bar',
|
|
||||||
'kind' => SymbolKind::VARIABLE,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 4
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => null
|
|
||||||
]
|
|
||||||
], json_decode(json_encode($symbols), true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ProjectTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->project->getDocument('file:///document1.php')->updateContent("<?php\nfunction foo() {}\nfunction bar() {}\n");
|
$this->project->getDocument('file:///document1.php')->updateContent("<?php\nfunction foo() {}\nfunction bar() {}\n");
|
||||||
$this->project->getDocument('file:///document2.php')->updateContent("<?php\nfunction baz() {}\nfunction frob() {}\n");
|
$this->project->getDocument('file:///document2.php')->updateContent("<?php\nfunction baz() {}\nfunction frob() {}\n");
|
||||||
|
$this->project->getDocument('invalid_file')->updateContent(file_get_contents(__DIR__ . '/../fixtures/invalid_file.php'));
|
||||||
|
|
||||||
$symbols = $this->project->findSymbols('ba');
|
$symbols = $this->project->findSymbols('ba');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentIdentifier, Position};
|
||||||
|
|
||||||
|
class DefinitionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$this->textDocument = new Server\TextDocument($project, $client);
|
||||||
|
$project->getDocument('references')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/references.php'));
|
||||||
|
$project->getDocument('symbols')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/symbols.php'));
|
||||||
|
$project->getDocument('use')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/use.php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForClassLike()
|
||||||
|
{
|
||||||
|
// $obj = new TestClass();
|
||||||
|
// Get definition for TestClass
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(4, 16));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 21,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForClassLikeUseStatement()
|
||||||
|
{
|
||||||
|
// use TestNamespace\TestClass;
|
||||||
|
// Get definition for TestClass
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(4, 22));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 21,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForClassLikeGroupUseStatement()
|
||||||
|
{
|
||||||
|
// use TestNamespace\{TestTrait, TestInterface};
|
||||||
|
// Get definition for TestInterface
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('use'), new Position(5, 37));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 28,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 31,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForImplements()
|
||||||
|
{
|
||||||
|
// class TestClass implements TestInterface
|
||||||
|
// Get definition for TestInterface
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('symbols'), new Position(6, 33));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 28,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 31,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForClassConstants()
|
||||||
|
{
|
||||||
|
// echo TestClass::TEST_CLASS_CONST;
|
||||||
|
// Get definition for TEST_CLASS_CONST
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(9, 21));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 8,
|
||||||
|
'character' => 10
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 8,
|
||||||
|
'character' => 32
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForConstants()
|
||||||
|
{
|
||||||
|
// echo TEST_CONST;
|
||||||
|
// Get definition for TEST_CONST
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(23, 9));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 4,
|
||||||
|
'character' => 6
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 4,
|
||||||
|
'character' => 22
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForStaticMethods()
|
||||||
|
{
|
||||||
|
// TestClass::staticTestMethod();
|
||||||
|
// Get definition for staticTestMethod
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(7, 20));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 12,
|
||||||
|
'character' => 4
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 15,
|
||||||
|
'character' => 5
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForStaticProperties()
|
||||||
|
{
|
||||||
|
// echo TestClass::$staticTestProperty;
|
||||||
|
// Get definition for staticTestProperty
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(8, 25));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 9,
|
||||||
|
'character' => 18
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 9,
|
||||||
|
'character' => 37
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForMethods()
|
||||||
|
{
|
||||||
|
// $obj->testMethod();
|
||||||
|
// Get definition for testMethod
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(5, 11));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 17,
|
||||||
|
'character' => 4
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 20,
|
||||||
|
'character' => 5
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForProperties()
|
||||||
|
{
|
||||||
|
// echo $obj->testProperty;
|
||||||
|
// Get definition for testProperty
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(6, 18));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 11
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 24
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForVariables()
|
||||||
|
{
|
||||||
|
// echo $var;
|
||||||
|
// Get definition for $var
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(13, 7));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'references',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 12,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 12,
|
||||||
|
'character' => 10
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForParamTypeHints()
|
||||||
|
{
|
||||||
|
// function whatever(TestClass $param) {
|
||||||
|
// Get definition for TestClass
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 23));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 21,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
public function testDefinitionForReturnTypeHints()
|
||||||
|
{
|
||||||
|
// function whatever(TestClass $param) {
|
||||||
|
// Get definition for TestClass
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(15, 42));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 21,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForParams()
|
||||||
|
{
|
||||||
|
// echo $param;
|
||||||
|
// Get definition for $param
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(16, 13));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'references',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 15,
|
||||||
|
'character' => 18
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 15,
|
||||||
|
'character' => 34
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForUsedVariables()
|
||||||
|
{
|
||||||
|
// echo $var;
|
||||||
|
// Get definition for $var
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(20, 11));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'references',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 19,
|
||||||
|
'character' => 22
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 19,
|
||||||
|
'character' => 26
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefinitionForFunctions()
|
||||||
|
{
|
||||||
|
// test_function();
|
||||||
|
// Get definition for test_function
|
||||||
|
$result = $this->textDocument->definition(new TextDocumentIdentifier('references'), new Position(10, 4));
|
||||||
|
$this->assertEquals([
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 33,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 36,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, Client, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{
|
||||||
|
TextDocumentIdentifier,
|
||||||
|
TextDocumentItem,
|
||||||
|
VersionedTextDocumentIdentifier,
|
||||||
|
TextDocumentContentChangeEvent,
|
||||||
|
Range,
|
||||||
|
Position
|
||||||
|
};
|
||||||
|
|
||||||
|
class DidChangeTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$textDocument = new Server\TextDocument($project, $client);
|
||||||
|
$phpDocument = $project->getDocument('whatever');
|
||||||
|
$phpDocument->updateContent("<?php\necho 'Hello, World'\n");
|
||||||
|
|
||||||
|
$identifier = new VersionedTextDocumentIdentifier('whatever');
|
||||||
|
$changeEvent = new TextDocumentContentChangeEvent();
|
||||||
|
$changeEvent->range = new Range(new Position(0, 0), new Position(9999, 9999));
|
||||||
|
$changeEvent->rangeLength = 9999;
|
||||||
|
$changeEvent->text = "<?php\necho 'Goodbye, World'\n";
|
||||||
|
|
||||||
|
$textDocument->didChange($identifier, [$changeEvent]);
|
||||||
|
|
||||||
|
$this->assertEquals("<?php\necho 'Goodbye, World'\n", $phpDocument->getContent());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentIdentifier, SymbolKind};
|
||||||
|
|
||||||
|
class DocumentSymbolTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$this->textDocument = new Server\TextDocument($project, $client);
|
||||||
|
$project->getDocument('symbols')->updateContent(file_get_contents(__DIR__ . '/../../../fixtures/symbols.php'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
// Request symbols
|
||||||
|
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier('symbols'));
|
||||||
|
$this->assertEquals([
|
||||||
|
[
|
||||||
|
'name' => 'TEST_CONST',
|
||||||
|
'kind' => SymbolKind::CONSTANT,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 4,
|
||||||
|
'character' => 6
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 4,
|
||||||
|
'character' => 22
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestClass',
|
||||||
|
'kind' => SymbolKind::CLASS_,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 6,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 21,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TEST_CLASS_CONST',
|
||||||
|
'kind' => SymbolKind::CONSTANT,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 8,
|
||||||
|
'character' => 10
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 8,
|
||||||
|
'character' => 32
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace\\TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'staticTestProperty',
|
||||||
|
'kind' => SymbolKind::PROPERTY,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 9,
|
||||||
|
'character' => 18
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 9,
|
||||||
|
'character' => 37
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace\\TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'testProperty',
|
||||||
|
'kind' => SymbolKind::PROPERTY,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 11
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 10,
|
||||||
|
'character' => 24
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace\\TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'staticTestMethod',
|
||||||
|
'kind' => SymbolKind::METHOD,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 12,
|
||||||
|
'character' => 4
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 15,
|
||||||
|
'character' => 5
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace\\TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'testMethod',
|
||||||
|
'kind' => SymbolKind::METHOD,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 17,
|
||||||
|
'character' => 4
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 20,
|
||||||
|
'character' => 5
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace\\TestClass'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestTrait',
|
||||||
|
'kind' => SymbolKind::CLASS_,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 23,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 26,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TestInterface',
|
||||||
|
'kind' => SymbolKind::INTERFACE,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 28,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 31,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'test_function',
|
||||||
|
'kind' => SymbolKind::FUNCTION,
|
||||||
|
'location' => [
|
||||||
|
'uri' => 'symbols',
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 33,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 36,
|
||||||
|
'character' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'containerName' => 'TestNamespace'
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, Client, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions};
|
||||||
|
|
||||||
|
class FormattingTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$this->textDocument = new Server\TextDocument($project, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$textDocument = new Server\TextDocument($project, $client);
|
||||||
|
|
||||||
|
// Trigger parsing of source
|
||||||
|
$textDocumentItem = new TextDocumentItem();
|
||||||
|
$textDocumentItem->uri = 'whatever';
|
||||||
|
$textDocumentItem->languageId = 'php';
|
||||||
|
$textDocumentItem->version = 1;
|
||||||
|
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/format.php');
|
||||||
|
$textDocument->didOpen($textDocumentItem);
|
||||||
|
|
||||||
|
// how code should look after formatting
|
||||||
|
$expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php');
|
||||||
|
// Request formatting
|
||||||
|
$result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions());
|
||||||
|
$this->assertEquals([0 => [
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 0,
|
||||||
|
'character' => 0
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => PHP_INT_MAX,
|
||||||
|
'character' => PHP_INT_MAX
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'newText' => $expected
|
||||||
|
]], json_decode(json_encode($result), true));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests\Server\TextDocument;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\Tests\MockProtocolStream;
|
||||||
|
use LanguageServer\{Server, Client, LanguageClient, Project};
|
||||||
|
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity};
|
||||||
|
|
||||||
|
class ParseErrorsTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Server\TextDocument
|
||||||
|
*/
|
||||||
|
private $textDocument;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$project = new Project($client);
|
||||||
|
$this->textDocument = new Server\TextDocument($project, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseErrorsArePublishedAsDiagnostics()
|
||||||
|
{
|
||||||
|
$args = null;
|
||||||
|
$client = new LanguageClient(new MockProtocolStream());
|
||||||
|
$client->textDocument = new class($args) extends Client\TextDocument {
|
||||||
|
private $args;
|
||||||
|
public function __construct(&$args)
|
||||||
|
{
|
||||||
|
parent::__construct(new MockProtocolStream());
|
||||||
|
$this->args = &$args;
|
||||||
|
}
|
||||||
|
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||||
|
{
|
||||||
|
$this->args = func_get_args();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$project = new Project($client);
|
||||||
|
|
||||||
|
$textDocument = new Server\TextDocument($project, $client);
|
||||||
|
|
||||||
|
// Trigger parsing of source
|
||||||
|
$textDocumentItem = new TextDocumentItem();
|
||||||
|
$textDocumentItem->uri = 'whatever';
|
||||||
|
$textDocumentItem->languageId = 'php';
|
||||||
|
$textDocumentItem->version = 1;
|
||||||
|
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/invalid_file.php');
|
||||||
|
$textDocument->didOpen($textDocumentItem);
|
||||||
|
$this->assertEquals([
|
||||||
|
'whatever',
|
||||||
|
[[
|
||||||
|
'range' => [
|
||||||
|
'start' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 10
|
||||||
|
],
|
||||||
|
'end' => [
|
||||||
|
'line' => 2,
|
||||||
|
'character' => 15
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'severity' => DiagnosticSeverity::ERROR,
|
||||||
|
'code' => null,
|
||||||
|
'source' => 'php',
|
||||||
|
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
|
||||||
|
]]
|
||||||
|
], json_decode(json_encode($args), true));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,241 +0,0 @@
|
||||||
<?php
|
|
||||||
declare(strict_types = 1);
|
|
||||||
|
|
||||||
namespace LanguageServer\Tests\Server;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use LanguageServer\Tests\MockProtocolStream;
|
|
||||||
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument};
|
|
||||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolKind, DiagnosticSeverity, FormattingOptions, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range, Position};
|
|
||||||
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
|
||||||
|
|
||||||
class TextDocumentTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDocumentSymbol()
|
|
||||||
{
|
|
||||||
$client = new LanguageClient(new MockProtocolStream());
|
|
||||||
$project = new Project($client);
|
|
||||||
$textDocument = new Server\TextDocument($project, $client);
|
|
||||||
// Trigger parsing of source
|
|
||||||
$textDocumentItem = new TextDocumentItem();
|
|
||||||
$textDocumentItem->uri = 'whatever';
|
|
||||||
$textDocumentItem->languageId = 'php';
|
|
||||||
$textDocumentItem->version = 1;
|
|
||||||
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/symbols.php');
|
|
||||||
$textDocument->didOpen($textDocumentItem);
|
|
||||||
// Request symbols
|
|
||||||
$result = $textDocument->documentSymbol(new TextDocumentIdentifier('whatever'));
|
|
||||||
$this->assertEquals([
|
|
||||||
[
|
|
||||||
'name' => 'TestNamespace',
|
|
||||||
'kind' => SymbolKind::NAMESPACE,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 24
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => null
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'TestClass',
|
|
||||||
'kind' => SymbolKind::CLASS_,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 4,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 12,
|
|
||||||
'character' => 1
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => 'TestNamespace'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'testProperty',
|
|
||||||
'kind' => SymbolKind::PROPERTY,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 6,
|
|
||||||
'character' => 11
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 6,
|
|
||||||
'character' => 24
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => 'TestNamespace\\TestClass'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'testMethod',
|
|
||||||
'kind' => SymbolKind::METHOD,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 8,
|
|
||||||
'character' => 4
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 11,
|
|
||||||
'character' => 5
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => 'TestNamespace\\TestClass'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'TestTrait',
|
|
||||||
'kind' => SymbolKind::CLASS_,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 14,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 17,
|
|
||||||
'character' => 1
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => 'TestNamespace'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'TestInterface',
|
|
||||||
'kind' => SymbolKind::INTERFACE,
|
|
||||||
'location' => [
|
|
||||||
'uri' => 'whatever',
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 19,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 22,
|
|
||||||
'character' => 1
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'containerName' => 'TestNamespace'
|
|
||||||
]
|
|
||||||
], json_decode(json_encode($result), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testParseErrorsArePublishedAsDiagnostics()
|
|
||||||
{
|
|
||||||
$args = null;
|
|
||||||
$client = new LanguageClient(new MockProtocolStream());
|
|
||||||
$client->textDocument = new class($args) extends Client\TextDocument {
|
|
||||||
private $args;
|
|
||||||
public function __construct(&$args)
|
|
||||||
{
|
|
||||||
parent::__construct(new MockProtocolStream());
|
|
||||||
$this->args = &$args;
|
|
||||||
}
|
|
||||||
public function publishDiagnostics(string $uri, array $diagnostics)
|
|
||||||
{
|
|
||||||
$this->args = func_get_args();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$project = new Project($client);
|
|
||||||
|
|
||||||
$textDocument = new Server\TextDocument($project, $client);
|
|
||||||
|
|
||||||
// Trigger parsing of source
|
|
||||||
$textDocumentItem = new TextDocumentItem();
|
|
||||||
$textDocumentItem->uri = 'whatever';
|
|
||||||
$textDocumentItem->languageId = 'php';
|
|
||||||
$textDocumentItem->version = 1;
|
|
||||||
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/invalid_file.php');
|
|
||||||
$textDocument->didOpen($textDocumentItem);
|
|
||||||
$this->assertEquals([
|
|
||||||
'whatever',
|
|
||||||
[[
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 10
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => 2,
|
|
||||||
'character' => 15
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'severity' => DiagnosticSeverity::ERROR,
|
|
||||||
'code' => null,
|
|
||||||
'source' => 'php',
|
|
||||||
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
|
|
||||||
]]
|
|
||||||
], json_decode(json_encode($args), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFormatting()
|
|
||||||
{
|
|
||||||
$client = new LanguageClient(new MockProtocolStream());
|
|
||||||
$project = new Project($client);
|
|
||||||
$textDocument = new Server\TextDocument($project, $client);
|
|
||||||
|
|
||||||
// Trigger parsing of source
|
|
||||||
$textDocumentItem = new TextDocumentItem();
|
|
||||||
$textDocumentItem->uri = 'whatever';
|
|
||||||
$textDocumentItem->languageId = 'php';
|
|
||||||
$textDocumentItem->version = 1;
|
|
||||||
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/format.php');
|
|
||||||
$textDocument->didOpen($textDocumentItem);
|
|
||||||
|
|
||||||
// how code should look after formatting
|
|
||||||
$expected = file_get_contents(__DIR__ . '/../../fixtures/format_expected.php');
|
|
||||||
// Request formatting
|
|
||||||
$result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions());
|
|
||||||
$this->assertEquals([0 => [
|
|
||||||
'range' => [
|
|
||||||
'start' => [
|
|
||||||
'line' => 0,
|
|
||||||
'character' => 0
|
|
||||||
],
|
|
||||||
'end' => [
|
|
||||||
'line' => PHP_INT_MAX,
|
|
||||||
'character' => PHP_INT_MAX
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'newText' => $expected
|
|
||||||
]], json_decode(json_encode($result), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDidChange()
|
|
||||||
{
|
|
||||||
$client = new LanguageClient(new MockProtocolStream());
|
|
||||||
$project = new Project($client);
|
|
||||||
$textDocument = new Server\TextDocument($project, $client);
|
|
||||||
|
|
||||||
$phpDocument = $project->getDocument('whatever');
|
|
||||||
$phpDocument->updateContent("<?php\necho 'Hello, World'\n");
|
|
||||||
|
|
||||||
$identifier = new VersionedTextDocumentIdentifier('whatever');
|
|
||||||
$changeEvent = new TextDocumentContentChangeEvent();
|
|
||||||
$changeEvent->range = new Range(new Position(0,0), new Position(9999,9999));
|
|
||||||
$changeEvent->rangeLength = 9999;
|
|
||||||
$changeEvent->text = "<?php\necho 'Goodbye, World'\n";
|
|
||||||
|
|
||||||
$textDocument->didChange($identifier, [$changeEvent]);
|
|
||||||
|
|
||||||
$this->assertEquals("<?php\necho 'Goodbye, World'\n", $phpDocument->getContent());
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue