1
0
Fork 0

Enable LS to operate without accessing the file system (#136)

This PR decouples the LS from direct file system access by implementing the proposals for workspace/files and textDocument/content under workspace/xfiles and textDocument/xcontent. The requests are only used when the client expressed support for them through ClientCapabilities, otherwise direct FS access is used.
This turns document content retrieval and recursive file search into async operations.
In turn, all server handlers can now operate async by returning a promise.
pull/147/head
Felix Becker 2016-11-14 10:25:44 +01:00 committed by GitHub
parent 25f300c157
commit 03bbf5f4ba
31 changed files with 799 additions and 386 deletions

View File

@ -29,7 +29,11 @@
"sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0",
"squizlabs/php_codesniffer" : "^2.7",
"symfony/debug": "^3.1"
"symfony/debug": "^3.1",
"netresearch/jsonmapper": "^1.0",
"webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1",
"sabre/uri": "^2.0"
},
"minimum-stability": "dev",
"prefer-stable": true,

View File

@ -1 +0,0 @@
A

View File

@ -1 +0,0 @@
B

View File

@ -1 +0,0 @@
Peeakboo!

View File

@ -4,8 +4,9 @@ declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use LanguageServer\Protocol\{Message, TextDocumentItem, TextDocumentIdentifier};
use Sabre\Event\Promise;
use JsonMapper;
/**
* Provides method handlers for all textDocument/* methods
@ -17,9 +18,15 @@ class TextDocument
*/
private $handler;
public function __construct(ClientHandler $handler)
/**
* @var JsonMapper
*/
private $mapper;
public function __construct(ClientHandler $handler, JsonMapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}
/**
@ -36,4 +43,21 @@ class TextDocument
'diagnostics' => $diagnostics
]);
}
/**
* The content request is sent from a server to a client
* to request the current content of a text document identified by the URI
*
* @param TextDocumentIdentifier $textDocument The document to get the content for
* @return Promise <TextDocumentItem> The document's current content
*/
public function xcontent(TextDocumentIdentifier $textDocument): Promise
{
return $this->handler->request(
'textDocument/xcontent',
['textDocument' => $textDocument]
)->then(function ($result) {
return $this->mapper->map($result, new TextDocumentItem);
});
}
}

47
src/Client/Workspace.php Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use LanguageServer\Protocol\TextDocumentIdentifier;
use Sabre\Event\Promise;
use JsonMapper;
/**
* Provides method handlers for all workspace/* methods
*/
class Workspace
{
/**
* @var ClientHandler
*/
private $handler;
/**
* @var JsonMapper
*/
private $mapper;
public function __construct(ClientHandler $handler, JsonMapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}
/**
* Returns a list of all files in a directory
*
* @param string $base The base directory (defaults to the workspace)
* @return Promise <TextDocumentIdentifier[]> Array of documents
*/
public function xfiles(string $base = null): Promise
{
return $this->handler->request(
'workspace/xfiles',
['base' => $base]
)->then(function (array $textDocuments) {
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
});
}
}

View File

@ -3,6 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer;
use JsonMapper;
class LanguageClient
{
/**
@ -19,11 +21,20 @@ class LanguageClient
*/
public $window;
/**
* Handles workspace/* methods
*
* @var Client\Workspace
*/
public $workspace;
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{
$handler = new ClientHandler($reader, $writer);
$mapper = new JsonMapper;
$this->textDocument = new Client\TextDocument($handler);
$this->textDocument = new Client\TextDocument($handler, $mapper);
$this->window = new Client\Window($handler);
$this->workspace = new Client\Workspace($handler, $mapper);
}
}

View File

@ -10,12 +10,18 @@ use LanguageServer\Protocol\{
Message,
MessageType,
InitializeResult,
SymbolInformation
SymbolInformation,
TextDocumentIdentifier
};
use AdvancedJsonRpc;
use Sabre\Event\Loop;
use Sabre\Event\{Loop, Promise};
use function Sabre\Event\coroutine;
use Exception;
use Throwable;
use Webmozart\Glob\Iterator\GlobIterator;
use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher
{
@ -38,6 +44,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
public $completionItem;
public $codeLens;
/**
* ClientCapabilities
*/
private $clientCapabilities;
private $protocolReader;
private $protocolWriter;
private $client;
@ -55,6 +66,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
parent::__construct($this, '/');
$this->protocolReader = $reader;
$this->protocolReader->on('message', function (Message $msg) {
coroutine(function () use ($msg) {
// Ignore responses, this is the handler for requests and notifications
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
return;
@ -63,13 +75,18 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$error = null;
try {
// Invoke the method handler to get a result
$result = $this->dispatch($msg->body);
$result = yield $this->dispatch($msg->body);
} catch (AdvancedJsonRpc\Error $e) {
// If a ResponseError is thrown, send it back in the Response
$error = $e;
} catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error($e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e);
$error = new AdvancedJsonRpc\Error(
$e->getMessage(),
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null,
$e
);
}
// Only send a Response for a Request
// Notifications do not send Responses
@ -81,14 +98,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
}
$this->protocolWriter->write(new Message($responseBody));
}
})->otherwise('\\LanguageServer\\crash');
});
$this->protocolWriter = $writer;
$this->client = new LanguageClient($reader, $writer);
$this->project = new Project($this->client);
$this->textDocument = new Server\TextDocument($this->project, $this->client);
$this->workspace = new Server\Workspace($this->project, $this->client);
}
/**
@ -102,10 +115,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult
{
$this->rootPath = $rootPath;
$this->clientCapabilities = $capabilities;
$this->project = new Project($this->client, $capabilities);
$this->textDocument = new Server\TextDocument($this->project, $this->client);
$this->workspace = new Server\Workspace($this->project, $this->client);
// start building project index
if ($rootPath !== null) {
$this->indexProject();
$this->indexProject()->otherwise('\\LanguageServer\\crash');
}
$serverCapabilities = new ServerCapabilities();
@ -136,6 +153,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/
public function shutdown()
{
unset($this->project);
}
/**
@ -151,42 +169,71 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
/**
* Parses workspace files, one at a time.
*
* @return void
* @return Promise <void>
*/
private function indexProject()
private function indexProject(): Promise
{
$fileList = findFilesRecursive($this->rootPath, '/^.+\.php$/i');
$numTotalFiles = count($fileList);
return coroutine(function () {
$textDocuments = yield $this->findPhpFiles();
$count = count($textDocuments);
$startTime = microtime(true);
$fileNum = 0;
$processFile = function () use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) {
if ($fileNum < $numTotalFiles) {
$file = $fileList[$fileNum];
$uri = pathToUri($file);
$fileNum++;
$shortName = substr($file, strlen($this->rootPath) + 1);
if (filesize($file) > 500000) {
$this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB");
} else {
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
yield Promise\all(array_map(function ($textDocument, $i) use ($count) {
return coroutine(function () use ($textDocument, $i, $count) {
// Give LS to the chance to handle requests while indexing
yield timeout();
$this->client->window->logMessage(
MessageType::INFO,
"Parsing file $i/$count: {$textDocument->uri}"
);
try {
$this->project->loadDocument($uri);
yield $this->project->loadDocument($textDocument->uri);
} catch (Exception $e) {
$this->client->window->logMessage(MessageType::ERROR, "Error parsing file $shortName: " . (string)$e);
}
$this->client->window->logMessage(
MessageType::ERROR,
"Error parsing file {$textDocument->uri}: " . (string)$e
);
}
});
}, $textDocuments, array_keys($textDocuments)));
Loop\setTimeout($processFile, 0);
} else {
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(MessageType::INFO, "All $numTotalFiles PHP files parsed in $duration seconds. $mem MiB allocated.");
$this->client->window->logMessage(
MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
});
}
};
Loop\setTimeout($processFile, 0);
/**
* Returns all PHP files in the workspace.
* If the client does not support workspace/files, it falls back to searching the file system directly.
*
* @return Promise <TextDocumentIdentifier[]>
*/
private function findPhpFiles(): Promise
{
return coroutine(function () {
$textDocuments = [];
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
if ($this->clientCapabilities->xfilesProvider) {
// Use xfiles request
foreach (yield $this->client->workspace->xfiles() as $textDocument) {
$path = Uri\parse($textDocument->uri)['path'];
if (Glob::match($path, $pattern)) {
$textDocuments[] = $textDocument;
}
}
} else {
// Use the file system
foreach (new GlobIterator($pattern) as $path) {
$textDocuments[] = new TextDocumentIdentifier(pathToUri($path));
yield timeout();
}
}
return $textDocuments;
});
}
}

View File

@ -17,6 +17,8 @@ use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory;
use function LanguageServer\Fqn\{getDefinedFqn, getVariableDefinition, getReferencedFqn};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class PhpDocument
{
@ -314,10 +316,11 @@ class PhpDocument
* The definition node MAY be in another document, check the ownerDocument attribute
*
* @param Node $node
* @return Node|null
* @return Promise <Node|null>
*/
public function getDefinitionByNode(Node $node)
public function getDefinitionByNode(Node $node): Promise
{
return coroutine(function () use ($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) {
@ -327,7 +330,7 @@ class PhpDocument
if (!isset($fqn)) {
return null;
}
$document = $this->project->getDefinitionDocument($fqn);
$document = yield $this->project->getDefinitionDocument($fqn);
if (!isset($document)) {
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
// http://php.net/manual/en/language.namespaces.fallback.php
@ -335,13 +338,14 @@ class PhpDocument
if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
$document = $this->project->getDefinitionDocument($fqn);
$document = yield $this->project->getDefinitionDocument($fqn);
}
}
if (!isset($document)) {
return null;
}
return $document->getDefinitionByFqn($fqn);
});
}
/**
@ -349,10 +353,11 @@ class PhpDocument
* The references node MAY be in other documents, check the ownerDocument attribute
*
* @param Node $node
* @return Node[]
* @return Promise <Node[]>
*/
public function getReferencesByNode(Node $node)
public function getReferencesByNode(Node $node): Promise
{
return coroutine(function () use ($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 || $node instanceof Node\Param) {
@ -378,7 +383,7 @@ class PhpDocument
if ($fqn === null) {
return [];
}
$refDocuments = $this->project->getReferenceDocuments($fqn);
$refDocuments = yield $this->project->getReferenceDocuments($fqn);
$nodes = [];
foreach ($refDocuments as $document) {
$refs = $document->getReferencesByFqn($fqn);
@ -389,5 +394,6 @@ class PhpDocument
}
}
return $nodes;
});
}
}

View File

@ -3,8 +3,10 @@ declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\SymbolInformation;
use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities};
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class Project
{
@ -51,40 +53,61 @@ class Project
*/
private $client;
public function __construct(LanguageClient $client)
/**
* The client's capabilities
*
* @var ClientCapabilities
*/
private $clientCapabilities;
public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities)
{
$this->client = $client;
$this->clientCapabilities = $clientCapabilities;
$this->parser = new Parser;
$this->docBlockFactory = DocBlockFactory::createInstance();
}
/**
* Returns the document indicated by uri.
* If the document is not open, tries to read it from disk, but the document is not added the list of open documents.
* Returns null if the document if not loaded.
*
* @param string $uri
* @return LanguageServer\PhpDocument
* @return PhpDocument|null
*/
public function getDocument(string $uri)
{
if (!isset($this->documents[$uri])) {
return $this->loadDocument($uri);
} else {
return $this->documents[$uri];
}
return $this->documents[$uri] ?? null;
}
/**
* Reads a document from disk.
* Returns the document indicated by uri.
* If the document is not open, loads it.
*
* @param string $uri
* @return Promise <PhpDocument>
*/
public function getOrLoadDocument(string $uri)
{
return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri);
}
/**
* Loads the document by doing a textDocument/xcontent request to the client.
* If the client does not support textDocument/xcontent, tries to read the file from the file system.
* The document is NOT added to the list of open documents, but definitions are registered.
*
* @param string $uri
* @return LanguageServer\PhpDocument
* @return Promise <PhpDocument>
*/
public function loadDocument(string $uri)
public function loadDocument(string $uri): Promise
{
return coroutine(function () use ($uri) {
if ($this->clientCapabilities->xcontentProvider) {
$content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text;
} else {
$content = file_get_contents(uriToPath($uri));
}
if (isset($this->documents[$uri])) {
$document = $this->documents[$uri];
$document->updateContent($content);
@ -92,6 +115,7 @@ class Project
$document = new PhpDocument($uri, $content, $this, $this->client, $this->parser, $this->docBlockFactory);
}
return $document;
});
}
/**
@ -222,14 +246,14 @@ class Project
* Returns all documents that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return PhpDocument[]
* @return Promise <PhpDocument[]>
*/
public function getReferenceDocuments(string $fqn)
public function getReferenceDocuments(string $fqn): Promise
{
if (!isset($this->references[$fqn])) {
return [];
return Promise\resolve([]);
}
return array_map([$this, 'getDocument'], $this->references[$fqn]);
return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn]));
}
/**
@ -258,11 +282,14 @@ class Project
* Returns the document where a symbol is defined
*
* @param string $fqn The fully qualified name of the symbol
* @return PhpDocument|null
* @return Promise <PhpDocument|null>
*/
public function getDefinitionDocument(string $fqn)
public function getDefinitionDocument(string $fqn): Promise
{
return isset($this->symbols[$fqn]) ? $this->getDocument($this->symbols[$fqn]->location->uri) : null;
if (!isset($this->symbols[$fqn])) {
return Promise\resolve(null);
}
return $this->getOrLoadDocument($this->symbols[$fqn]->location->uri);
}
/**

View File

@ -4,5 +4,17 @@ namespace LanguageServer\Protocol;
class ClientCapabilities
{
/**
* The client supports workspace/xfiles requests
*
* @var bool|null
*/
public $xfilesProvider;
/**
* The client supports textDocument/xcontent requests
*
* @var bool|null
*/
public $xcontentProvider;
}

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project};
use LanguageServer\{LanguageClient, Project, PhpDocument};
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\Node;
use LanguageServer\Protocol\{
@ -20,6 +20,8 @@ use LanguageServer\Protocol\{
Hover,
MarkedString
};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
/**
* Provides method handlers for all textDocument/* methods
@ -55,11 +57,13 @@ class TextDocument
* document.
*
* @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument
* @return SymbolInformation[]
* @return Promise <SymbolInformation[]>
*/
public function documentSymbol(TextDocumentIdentifier $textDocument): array
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
{
return array_values($this->project->getDocument($textDocument->uri)->getSymbols());
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) {
return array_values($document->getSymbols());
});
}
/**
@ -105,11 +109,13 @@ class TextDocument
*
* @param TextDocumentIdentifier $textDocument The document to format
* @param FormattingOptions $options The format options
* @return TextEdit[]
* @return Promise <TextEdit[]>
*/
public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options)
{
return $this->project->getDocument($textDocument->uri)->getFormattedText();
return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) {
return $document->getFormattedText();
});
}
/**
@ -117,21 +123,26 @@ class TextDocument
* denoted by the given text document position.
*
* @param ReferenceContext $context
* @return Location[]
* @return Promise <Location[]>
*/
public function references(ReferenceContext $context, TextDocumentIdentifier $textDocument, Position $position): array
{
$document = $this->project->getDocument($textDocument->uri);
public function references(
ReferenceContext $context,
TextDocumentIdentifier $textDocument,
Position $position
): Promise {
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri);
$node = $document->getNodeAtPosition($position);
if ($node === null) {
return [];
}
$refs = $document->getReferencesByNode($node);
$refs = yield $document->getReferencesByNode($node);
$locations = [];
foreach ($refs as $ref) {
$locations[] = Location::fromNode($ref);
}
return $locations;
});
}
/**
@ -140,20 +151,22 @@ class TextDocument
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
* @return Location|Location[]
* @return Promise <Location|Location[]>
*/
public function definition(TextDocumentIdentifier $textDocument, Position $position)
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
{
$document = $this->project->getDocument($textDocument->uri);
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri);
$node = $document->getNodeAtPosition($position);
if ($node === null) {
return [];
}
$def = $document->getDefinitionByNode($node);
$def = yield $document->getDefinitionByNode($node);
if ($def === null) {
return [];
}
return Location::fromNode($def);
});
}
/**
@ -161,11 +174,12 @@ class TextDocument
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
* @return Hover
* @return Promise <Hover>
*/
public function hover(TextDocumentIdentifier $textDocument, Position $position): Hover
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
{
$document = $this->project->getDocument($textDocument->uri);
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->project->getOrLoadDocument($textDocument->uri);
// Find the node under the cursor
$node = $document->getNodeAtPosition($position);
if ($node === null) {
@ -173,7 +187,7 @@ class TextDocument
}
$range = Range::fromNode($node);
// Get the definition node for whatever node is under the cursor
$def = $document->getDefinitionByNode($node);
$def = yield $document->getDefinitionByNode($node);
if ($def === null) {
return new Hover([], $range);
}
@ -222,5 +236,6 @@ class TextDocument
}
return new Hover($contents, $range);
});
}
}

View File

@ -3,26 +3,9 @@ declare(strict_types = 1);
namespace LanguageServer;
use Throwable;
use InvalidArgumentException;
/**
* Recursively Searches files with matching filename, starting at $path.
*
* @param string $path
* @param string $pattern
* @return array
*/
function findFilesRecursive(string $path, string $pattern): array
{
$dir = new \RecursiveDirectoryIterator($path);
$ite = new \RecursiveIteratorIterator($dir);
$files = new \RegexIterator($ite, $pattern, \RegexIterator::GET_MATCH);
$fileList = [];
foreach ($files as $file) {
$fileList = array_merge($fileList, $file);
}
return $fileList;
}
use Sabre\Event\{Loop, Promise};
/**
* Transforms an absolute file path into a URI as used by the language server protocol.
@ -66,3 +49,31 @@ function uriToPath(string $uri)
}
return $filepath;
}
/**
* Throws an exception on the next tick.
* Useful for letting a promise crash the process on rejection.
*
* @param Throwable $err
* @return void
*/
function crash(Throwable $err)
{
Loop\nextTick(function () use ($err) {
throw $err;
});
}
/**
* Returns a promise that is resolved after x seconds.
* Useful for giving back control to the event loop inside a coroutine.
*
* @param int $seconds
* @return Promise <void>
*/
function timeout($seconds = 0): Promise
{
$promise = new Promise;
Loop\setTimeout([$promise, 'fulfill'], $seconds);
return $promise;
}

View File

@ -5,9 +5,13 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer;
use LanguageServer\Protocol\{Message, ClientCapabilities, TextDocumentSyncKind, MessageType};
use LanguageServer\Protocol\{
Message, ClientCapabilities, TextDocumentSyncKind, MessageType, TextDocumentItem, TextDocumentIdentifier};
use AdvancedJsonRpc;
use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path;
use Sabre\Event\Promise;
use Exception;
use function LanguageServer\pathToUri;
class LanguageServerTest extends TestCase
@ -17,15 +21,14 @@ class LanguageServerTest extends TestCase
$reader = new MockProtocolStream();
$writer = new MockProtocolStream();
$server = new LanguageServer($reader, $writer);
$msg = null;
$writer->on('message', function (Message $message) use (&$msg) {
$msg = $message;
});
$promise = new Promise;
$writer->once('message', [$promise, 'fulfill']);
$reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [
'rootPath' => __DIR__,
'processId' => getmypid(),
'capabilities' => new ClientCapabilities()
])));
$msg = $promise->wait();
$this->assertNotNull($msg, 'message event should be emitted');
$this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body);
$this->assertEquals((object)[
@ -49,7 +52,7 @@ class LanguageServerTest extends TestCase
], $msg->body->result);
}
public function testIndexing()
public function testIndexingWithDirectFileAccess()
{
$promise = new Promise;
$input = new MockProtocolStream;
@ -68,4 +71,54 @@ class LanguageServerTest extends TestCase
$server->initialize(getmypid(), $capabilities, realpath(__DIR__ . '/../fixtures'));
$promise->wait();
}
public function testIndexingWithFilesAndContentRequests()
{
$promise = new Promise;
$filesCalled = false;
$contentCalled = false;
$rootPath = realpath(__DIR__ . '/../fixtures');
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled) {
if ($msg->body->method === 'textDocument/xcontent') {
// Document content requested
$contentCalled = true;
$textDocumentItem = new TextDocumentItem;
$textDocumentItem->uri = $msg->body->params->textDocument->uri;
$textDocumentItem->version = 1;
$textDocumentItem->languageId = 'php';
$textDocumentItem->text = file_get_contents($msg->body->params->textDocument->uri);
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, $textDocumentItem)));
} else if ($msg->body->method === 'workspace/xfiles') {
// Files requested
$filesCalled = true;
$pattern = Path::makeAbsolute('**/*.php', $msg->body->params->base ?? $rootPath);
$files = [];
foreach (Glob::glob($pattern) as $path) {
$files[] = new TextDocumentIdentifier(pathToUri($path));
}
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, $files)));
} else if ($msg->body->method === 'window/logMessage') {
// Message logged
if ($msg->body->params->type === MessageType::ERROR) {
// Error happened during indexing, fail test
if ($promise->state === Promise::PENDING) {
$promise->reject(new Exception($msg->body->params->message));
}
} else if (strpos($msg->body->params->message, 'All 10 PHP files parsed') !== false) {
// Indexing finished
$promise->fulfill();
}
}
});
$server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities;
$capabilities->xfilesProvider = true;
$capabilities->xcontentProvider = true;
$server->initialize(getmypid(), $capabilities, $rootPath);
$promise->wait();
$this->assertTrue($filesCalled);
$this->assertTrue($contentCalled);
}
}

View File

@ -5,7 +5,7 @@ namespace LanguageServer\Tests;
use LanguageServer\{ProtocolReader, ProtocolWriter};
use LanguageServer\Protocol\Message;
use Sabre\Event\{Emitter, Promise};
use Sabre\Event\{Loop, Emitter, Promise};
/**
* A fake duplex protocol stream
@ -20,7 +20,9 @@ class MockProtocolStream extends Emitter implements ProtocolReader, ProtocolWrit
*/
public function write(Message $msg): Promise
{
Loop\nextTick(function () use ($msg) {
$this->emit('message', [Message::parse((string)$msg)]);
});
return Promise\resolve(null);
}
}

View File

@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
use PhpParser\{NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\{LanguageClient, Project, PhpDocument, Parser};
use LanguageServer\Protocol\ClientCapabilities;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
use function LanguageServer\pathToUri;
@ -16,10 +17,10 @@ class DefinitionCollectorTest extends TestCase
public function testCollectsSymbols()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php'));
$document = $project->loadDocument($uri);
$document = $project->loadDocument($uri)->wait();
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));
@ -55,10 +56,10 @@ class DefinitionCollectorTest extends TestCase
public function testDoesNotCollectReferences()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$parser = new Parser;
$uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
$document = $project->loadDocument($uri);
$document = $project->loadDocument($uri)->wait();
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));

View File

@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{LanguageClient, Project};
use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\Protocol\{SymbolKind, Position};
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use PhpParser\Node;
class PhpDocumentTest extends TestCase
@ -19,7 +19,8 @@ class PhpDocumentTest extends TestCase
public function setUp()
{
$this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new ClientCapabilities);
}
public function testParsesVariableVariables()

View File

@ -6,7 +6,14 @@ 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};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
SymbolKind,
DiagnosticSeverity,
FormattingOptions,
ClientCapabilities
};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use function LanguageServer\pathToUri;
@ -19,12 +26,13 @@ class ProjectTest extends TestCase
public function setUp()
{
$this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream));
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client, new ClientCapabilities);
}
public function testGetDocumentLoadsDocument()
public function testGetOrLoadDocumentLoadsDocument()
{
$document = $this->project->getDocument(pathToUri(__FILE__));
$document = $this->project->getOrLoadDocument(pathToUri(__FILE__))->wait();
$this->assertNotNull($document);
$this->assertInstanceOf(PhpDocument::class, $document);

View File

@ -6,8 +6,9 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{Position, Location, Range};
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities};
use function LanguageServer\pathToUri;
use Sabre\Event\Promise;
abstract class ServerTestCase extends TestCase
{
@ -43,7 +44,7 @@ abstract class ServerTestCase extends TestCase
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->project = new Project($client);
$this->project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($this->project, $client);
$this->workspace = new Server\Workspace($this->project, $client);
@ -53,11 +54,13 @@ abstract class ServerTestCase extends TestCase
$referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php'));
$useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php'));
$this->project->loadDocument($symbolsUri);
$this->project->loadDocument($referencesUri);
$this->project->loadDocument($globalSymbolsUri);
$this->project->loadDocument($globalReferencesUri);
$this->project->loadDocument($useUri);
Promise\all([
$this->project->loadDocument($symbolsUri),
$this->project->loadDocument($referencesUri),
$this->project->loadDocument($globalSymbolsUri),
$this->project->loadDocument($globalReferencesUri),
$this->project->loadDocument($useUri)
])->wait();
// @codingStandardsIgnoreStart
$this->definitionLocations = [

View File

@ -6,14 +6,15 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities};
use Sabre\Event\Promise;
class GlobalFallbackTest extends ServerTestCase
{
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($project, $client);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
@ -23,7 +24,10 @@ class GlobalFallbackTest extends ServerTestCase
{
// $obj = new TestClass();
// Get definition for TestClass should not fall back to global
$result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(9, 16));
$result = $this->textDocument->definition(
new TextDocumentIdentifier('global_fallback'),
new Position(9, 16)
)->wait();
$this->assertEquals([], $result);
}
@ -31,7 +35,10 @@ class GlobalFallbackTest extends ServerTestCase
{
// echo TEST_CONST;
// Get definition for TEST_CONST
$result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(6, 10));
$result = $this->textDocument->definition(
new TextDocumentIdentifier('global_fallback'),
new Position(6, 10)
)->wait();
$this->assertEquals(new Location('global_symbols', new Range(new Position(9, 6), new Position(9, 22))), $result);
}
@ -39,7 +46,10 @@ class GlobalFallbackTest extends ServerTestCase
{
// test_function();
// Get definition for test_function
$result = $this->textDocument->definition(new TextDocumentIdentifier('global_fallback'), new Position(5, 6));
$result = $this->textDocument->definition(
new TextDocumentIdentifier('global_fallback'),
new Position(5, 6)
)->wait();
$this->assertEquals(new Location('global_symbols', new Range(new Position(78, 0), new Position(81, 1))), $result);
}
}

View File

@ -12,14 +12,20 @@ class GlobalTest extends ServerTestCase
public function testDefinitionFileBeginning()
{
// |<?php
$result = $this->textDocument->definition(new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))), new Position(0, 0));
$result = $this->textDocument->definition(
new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))),
new Position(0, 0)
)->wait();
$this->assertEquals([], $result);
}
public function testDefinitionEmptyResult()
{
// namespace keyword
$result = $this->textDocument->definition(new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))), new Position(2, 4));
$result = $this->textDocument->definition(
new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))),
new Position(2, 4)
)->wait();
$this->assertEquals([], $result);
}
@ -28,7 +34,10 @@ class GlobalTest extends ServerTestCase
// $obj = new TestClass();
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -37,7 +46,10 @@ class GlobalTest extends ServerTestCase
// TestClass::staticTestMethod();
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -46,7 +58,10 @@ class GlobalTest extends ServerTestCase
// echo TestClass::$staticTestProperty;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[2];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -55,7 +70,10 @@ class GlobalTest extends ServerTestCase
// TestClass::TEST_CLASS_CONST;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[3];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -64,7 +82,10 @@ class GlobalTest extends ServerTestCase
// class TestClass implements TestInterface
// Get definition for TestInterface
$reference = $this->getReferenceLocations('TestInterface')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestInterface'), $result);
}
@ -73,7 +94,10 @@ class GlobalTest extends ServerTestCase
// echo TestClass::TEST_CLASS_CONST;
// Get definition for TEST_CLASS_CONST
$reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result);
}
@ -82,7 +106,10 @@ class GlobalTest extends ServerTestCase
// echo self::TEST_CLASS_CONST;
// Get definition for TEST_CLASS_CONST
$reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::TEST_CLASS_CONST'), $result);
}
@ -91,7 +118,10 @@ class GlobalTest extends ServerTestCase
// echo TEST_CONST;
// Get definition for TEST_CONST
$reference = $this->getReferenceLocations('TEST_CONST')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TEST_CONST'), $result);
}
@ -100,7 +130,10 @@ class GlobalTest extends ServerTestCase
// TestClass::staticTestMethod();
// Get definition for staticTestMethod
$reference = $this->getReferenceLocations('TestClass::staticTestMethod()')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::staticTestMethod()'), $result);
}
@ -109,7 +142,10 @@ class GlobalTest extends ServerTestCase
// echo TestClass::$staticTestProperty;
// Get definition for staticTestProperty
$reference = $this->getReferenceLocations('TestClass::staticTestProperty')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::staticTestProperty'), $result);
}
@ -118,7 +154,10 @@ class GlobalTest extends ServerTestCase
// $obj->testMethod();
// Get definition for testMethod
$reference = $this->getReferenceLocations('TestClass::testMethod()')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::testMethod()'), $result);
}
@ -127,7 +166,10 @@ class GlobalTest extends ServerTestCase
// echo $obj->testProperty;
// Get definition for testProperty
$reference = $this->getReferenceLocations('TestClass::testProperty')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result);
}
@ -136,7 +178,10 @@ class GlobalTest extends ServerTestCase
// $this->testProperty = $testParameter;
// Get definition for testProperty
$reference = $this->getReferenceLocations('TestClass::testProperty')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass::testProperty'), $result);
}
@ -145,7 +190,10 @@ class GlobalTest extends ServerTestCase
// echo $var;
// Get definition for $var
$uri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$result = $this->textDocument->definition(new TextDocumentIdentifier($uri), new Position(13, 7));
$result = $this->textDocument->definition(
new TextDocumentIdentifier($uri),
new Position(13, 7)
)->wait();
$this->assertEquals(new Location($uri, new Range(new Position(12, 0), new Position(12, 10))), $result);
}
@ -154,7 +202,10 @@ class GlobalTest extends ServerTestCase
// function whatever(TestClass $param) {
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[4];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -163,7 +214,10 @@ class GlobalTest extends ServerTestCase
// function whatever(TestClass $param): TestClass {
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[5];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -172,7 +226,10 @@ class GlobalTest extends ServerTestCase
// public function testMethod($testParameter): TestInterface
// Get definition for TestInterface
$reference = $this->getReferenceLocations('TestInterface')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestInterface'), $result);
}
@ -181,7 +238,10 @@ class GlobalTest extends ServerTestCase
// echo $param;
// Get definition for $param
$uri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$result = $this->textDocument->definition(new TextDocumentIdentifier($uri), new Position(22, 13));
$result = $this->textDocument->definition(
new TextDocumentIdentifier($uri),
new Position(22, 13)
)->wait();
$this->assertEquals(new Location($uri, new Range(new Position(21, 18), new Position(21, 34))), $result);
}
@ -190,7 +250,10 @@ class GlobalTest extends ServerTestCase
// echo $var;
// Get definition for $var
$uri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$result = $this->textDocument->definition(new TextDocumentIdentifier($uri), new Position(26, 11));
$result = $this->textDocument->definition(
new TextDocumentIdentifier($uri),
new Position(26, 11)
)->wait();
$this->assertEquals(new Location($uri, new Range(new Position(25, 22), new Position(25, 26))), $result);
}
@ -199,7 +262,10 @@ class GlobalTest extends ServerTestCase
// test_function();
// Get definition for test_function
$reference = $this->getReferenceLocations('test_function()')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('test_function()'), $result);
}
@ -208,7 +274,10 @@ class GlobalTest extends ServerTestCase
// use function test_function;
// Get definition for test_function
$reference = $this->getReferenceLocations('test_function()')[1];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('test_function()'), $result);
}
@ -217,7 +286,10 @@ class GlobalTest extends ServerTestCase
// if ($abc instanceof TestInterface) {
// Get definition for TestInterface
$reference = $this->getReferenceLocations('TestInterface')[2];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestInterface'), $result);
}
}

View File

@ -23,7 +23,10 @@ class NamespacedTest extends GlobalTest
// echo TEST_CONST;
// Get definition for TEST_CONST
$reference = $this->getReferenceLocations('TEST_CONST')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TEST_CONST'), $result);
}
@ -32,7 +35,10 @@ class NamespacedTest extends GlobalTest
// use TestNamespace\TestClass;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[6];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
@ -41,7 +47,10 @@ class NamespacedTest extends GlobalTest
// use TestNamespace\{TestTrait, TestInterface};
// Get definition for TestInterface
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->definition(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
}

View File

@ -12,7 +12,8 @@ use LanguageServer\Protocol\{
VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent,
Range,
Position
Position,
ClientCapabilities
};
class DidChangeTest extends TestCase
@ -20,7 +21,7 @@ class DidChangeTest extends TestCase
public function test()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->openDocument('whatever', "<?php\necho 'Hello, World'\n");

View File

@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
use Exception;
class DidCloseTest extends TestCase
@ -14,7 +14,7 @@ class DidCloseTest extends TestCase
public function test()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$textDocument = new Server\TextDocument($project, $client);
$phpDocument = $project->openDocument('whatever', 'hello world');

View File

@ -15,7 +15,7 @@ class DocumentSymbolTest extends ServerTestCase
{
// Request symbols
$uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/symbols.php'));
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri));
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait();
// @codingStandardsIgnoreStart
$this->assertEquals([
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),

View File

@ -6,7 +6,15 @@ 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};
use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
FormattingOptions,
ClientCapabilities,
TextEdit,
Range,
Position
};
use function LanguageServer\{pathToUri, uriToPath};
class FormattingTest extends TestCase
@ -19,14 +27,14 @@ class FormattingTest extends TestCase
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($project, $client);
}
public function testFormatting()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$textDocument = new Server\TextDocument($project, $client);
$path = realpath(__DIR__ . '/../../../fixtures/format.php');
$uri = pathToUri($path);
@ -42,19 +50,7 @@ class FormattingTest extends TestCase
// how code should look after formatting
$expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php');
// Request formatting
$result = $textDocument->formatting(new TextDocumentIdentifier($uri), new FormattingOptions());
$this->assertEquals([0 => [
'range' => [
'start' => [
'line' => 0,
'character' => 0
],
'end' => [
'line' => 20,
'character' => 0
]
],
'newText' => $expected
]], json_decode(json_encode($result), true));
$result = $textDocument->formatting(new TextDocumentIdentifier($uri), new FormattingOptions())->wait();
$this->assertEquals([new TextEdit(new Range(new Position(0, 0), new Position(20, 0)), $expected)], $result);
}
}

View File

@ -16,7 +16,10 @@ class HoverTest extends ServerTestCase
// $obj = new TestClass();
// Get hover for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->start);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nclass TestClass implements \\TestInterface"),
'Pariatur ut laborum tempor voluptate consequat ea deserunt.'
@ -28,7 +31,10 @@ class HoverTest extends ServerTestCase
// $obj->testMethod();
// Get hover for testMethod
$reference = $this->getReferenceLocations('TestClass::testMethod()')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\npublic function testMethod(\$testParameter) : \TestInterface"),
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
@ -40,7 +46,10 @@ class HoverTest extends ServerTestCase
// echo $obj->testProperty;
// Get hover for testProperty
$reference = $this->getReferenceLocations('TestClass::testProperty')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\npublic \$testProperty;"),
'Reprehenderit magna velit mollit ipsum do.'
@ -52,7 +61,10 @@ class HoverTest extends ServerTestCase
// TestClass::staticTestMethod();
// Get hover for staticTestMethod
$reference = $this->getReferenceLocations('TestClass::staticTestMethod()')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\npublic static function staticTestMethod()"),
'Do magna consequat veniam minim proident eiusmod incididunt aute proident.'
@ -64,7 +76,10 @@ class HoverTest extends ServerTestCase
// echo TestClass::staticTestProperty;
// Get hover for staticTestProperty
$reference = $this->getReferenceLocations('TestClass::staticTestProperty')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\npublic static \$staticTestProperty;"),
'Lorem excepteur officia sit anim velit veniam enim.'
@ -76,7 +91,10 @@ class HoverTest extends ServerTestCase
// echo TestClass::TEST_CLASS_CONST;
// Get hover for TEST_CLASS_CONST
$reference = $this->getReferenceLocations('TestClass::TEST_CLASS_CONST')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nconst TEST_CLASS_CONST = 123;"),
'Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.'
@ -88,7 +106,10 @@ class HoverTest extends ServerTestCase
// test_function();
// Get hover for test_function
$reference = $this->getReferenceLocations('test_function()')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nfunction test_function()"),
'Officia aliquip adipisicing et nulla et laboris dolore labore.'
@ -100,7 +121,10 @@ class HoverTest extends ServerTestCase
// echo TEST_CONST;
// Get hover for TEST_CONST
$reference = $this->getReferenceLocations('TEST_CONST')[0];
$result = $this->textDocument->hover(new TextDocumentIdentifier($reference->uri), $reference->range->end);
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nconst TEST_CONST = 123;"),
'Esse commodo excepteur pariatur Lorem est aute incididunt reprehenderit.'
@ -112,7 +136,7 @@ class HoverTest extends ServerTestCase
// echo $var;
// Get hover for $var
$uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(13, 7));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(13, 7))->wait();
$this->assertEquals(new Hover(
[new MarkedString('php', "<?php\n\$var = 123;")],
new Range(new Position(13, 5), new Position(13, 9))
@ -124,7 +148,7 @@ class HoverTest extends ServerTestCase
// echo $param;
// Get hover for $param
$uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(22, 11));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(22, 11))->wait();
$this->assertEquals(new Hover(
[
new MarkedString('php', "<?php\n\TestNamespace\TestClass \$param"),

View File

@ -6,8 +6,9 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities};
use Sabre\Event\Promise;
use JsonMapper;
class ParseErrorsTest extends TestCase
{
@ -25,7 +26,7 @@ class ParseErrorsTest extends TestCase
private $args;
public function __construct(&$args)
{
parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream));
parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream), new JsonMapper);
$this->args = &$args;
}
public function publishDiagnostics(string $uri, array $diagnostics): Promise
@ -34,7 +35,7 @@ class ParseErrorsTest extends TestCase
return Promise\resolve(null);
}
};
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($project, $client);
}

View File

@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument\References;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range};
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities};
use LanguageServer\Tests\Server\ServerTestCase;
class GlobalFallbackTest extends ServerTestCase
@ -14,7 +14,7 @@ class GlobalFallbackTest extends ServerTestCase
public function setUp()
{
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$project = new Project($client);
$project = new Project($client, new ClientCapabilities);
$this->textDocument = new Server\TextDocument($project, $client);
$project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php'));
$project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));
@ -24,7 +24,11 @@ class GlobalFallbackTest extends ServerTestCase
{
// class TestClass implements TestInterface
// Get references for TestClass
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(6, 9));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier('global_symbols'),
new Position(6, 9)
)->wait();
$this->assertEquals([], $result);
}
@ -32,7 +36,11 @@ class GlobalFallbackTest extends ServerTestCase
{
// const TEST_CONST = 123;
// Get references for TEST_CONST
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(9, 13));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier('global_symbols'),
new Position(9, 13)
)->wait();
$this->assertEquals([new Location('global_fallback', new Range(new Position(6, 5), new Position(6, 15)))], $result);
}
@ -40,7 +48,11 @@ class GlobalFallbackTest extends ServerTestCase
{
// function test_function()
// Get references for test_function
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier('global_symbols'), new Position(78, 16));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier('global_symbols'),
new Position(78, 16)
)->wait();
$this->assertEquals([new Location('global_fallback', new Range(new Position(5, 0), new Position(5, 13)))], $result);
}
}

View File

@ -14,7 +14,11 @@ class GlobalTest extends ServerTestCase
// class TestClass implements TestInterface
// Get references for TestClass
$definition = $this->getDefinitionLocation('TestClass');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass'), $result);
}
@ -23,7 +27,11 @@ class GlobalTest extends ServerTestCase
// const TEST_CLASS_CONST = 123;
// Get references for TEST_CLASS_CONST
$definition = $this->getDefinitionLocation('TestClass::TEST_CLASS_CONST');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass::TEST_CLASS_CONST'), $result);
}
@ -32,7 +40,11 @@ class GlobalTest extends ServerTestCase
// const TEST_CONST = 123;
// Get references for TEST_CONST
$definition = $this->getDefinitionLocation('TEST_CONST');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TEST_CONST'), $result);
}
@ -41,7 +53,11 @@ class GlobalTest extends ServerTestCase
// public static function staticTestMethod()
// Get references for staticTestMethod
$definition = $this->getDefinitionLocation('TestClass::staticTestMethod()');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass::staticTestMethod()'), $result);
}
@ -50,7 +66,11 @@ class GlobalTest extends ServerTestCase
// public static $staticTestProperty;
// Get references for $staticTestProperty
$definition = $this->getDefinitionLocation('TestClass::staticTestProperty');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass::staticTestProperty'), $result);
}
@ -59,7 +79,11 @@ class GlobalTest extends ServerTestCase
// public function testMethod($testParameter)
// Get references for testMethod
$definition = $this->getDefinitionLocation('TestClass::testMethod()');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass::testMethod()'), $result);
}
@ -68,7 +92,11 @@ class GlobalTest extends ServerTestCase
// public $testProperty;
// Get references for testProperty
$definition = $this->getDefinitionLocation('TestClass::testProperty');
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($definition->uri), $definition->range->start);
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($definition->uri),
$definition->range->start
)->wait();
$this->assertEquals($this->getReferenceLocations('TestClass::testProperty'), $result);
}
@ -77,7 +105,11 @@ class GlobalTest extends ServerTestCase
// $var = 123;
// Get definition for $var
$uri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($uri), new Position(12, 3));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($uri),
new Position(12, 3)
)->wait();
$this->assertEquals([
new Location($uri, new Range(new Position(12, 0), new Position(12, 4))),
new Location($uri, new Range(new Position(13, 5), new Position(13, 9))),
@ -90,7 +122,11 @@ class GlobalTest extends ServerTestCase
// function whatever(TestClass $param): TestClass
// Get references for $param
$uri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($uri), new Position(21, 32));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($uri),
new Position(21, 32)
)->wait();
$this->assertEquals([new Location($uri, new Range(new Position(22, 9), new Position(22, 15)))], $result);
}
@ -100,7 +136,11 @@ class GlobalTest extends ServerTestCase
// Get references for test_function
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'));
$symbolsUri = pathToUri(realpath(__DIR__ . '/../../../../fixtures/symbols.php'));
$result = $this->textDocument->references(new ReferenceContext, new TextDocumentIdentifier($symbolsUri), new Position(78, 16));
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($symbolsUri),
new Position(78, 16)
)->wait();
$this->assertEquals([
new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40)))

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Utils;
use PHPUnit\Framework\TestCase;
class RecursiveFileSearchTest extends TestCase
{
public function testFilesAreFound()
{
$path = realpath(__DIR__ . '/../../fixtures/recursive');
$files = \LanguageServer\findFilesRecursive($path, '/.+\.txt/');
sort($files);
$this->assertEquals([
$path . DIRECTORY_SEPARATOR . 'a.txt',
$path . DIRECTORY_SEPARATOR . 'search' . DIRECTORY_SEPARATOR . 'b.txt',
$path . DIRECTORY_SEPARATOR . 'search' . DIRECTORY_SEPARATOR . 'here' . DIRECTORY_SEPARATOR . 'c.txt',
], $files);
}
}