1
0
Fork 0
pull/308/merge
Juergen Steitz 2018-08-31 22:10:49 +00:00 committed by GitHub
commit 1be249cfdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 696 additions and 41 deletions

View File

@ -0,0 +1 @@
<?php

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\ConfigurationItem;
use LanguageServer\Protocol\TextDocumentIdentifier; use LanguageServer\Protocol\TextDocumentIdentifier;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;
@ -44,4 +45,24 @@ class Workspace
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class); return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
}); });
} }
/**
* The workspace/configuration request is sent from the server to the
* client to fetch configuration settings from the client.
*
* The request can fetch n configuration settings in one roundtrip.
* The order of the returned configuration settings correspond to the order
* of the passed ConfigurationItems (e.g. the first item in the response is
* the result for the first configuration item in the params).
*
* @param ConfigurationItem[] $items
* @return Promise <mixed[]>
*/
public function configuration(array $items): Promise
{
return $this->handler->request(
'workspace/configuration',
['items' => $items]
);
}
} }

View File

@ -147,4 +147,14 @@ abstract class AbstractAggregateIndex implements ReadableIndex
} }
return $refs; return $refs;
} }
/**
* Wipe all indexes for a reindex
*/
public function wipe()
{
foreach ($this->getIndexes() as $index) {
$index->wipe();
}
}
} }

View File

@ -222,4 +222,15 @@ class Index implements ReadableIndex, \Serializable
'staticComplete' => $this->staticComplete 'staticComplete' => $this->staticComplete
]); ]);
} }
/**
* Clear indexed references and definitions
*/
public function wipe()
{
$this->definitions = [];
$this->references = [];
$this->complete = false;
$this->staticComplete = false;
}
} }

View File

@ -35,7 +35,7 @@ class ProjectIndex extends AbstractAggregateIndex
/** /**
* @return ReadableIndex[] * @return ReadableIndex[]
*/ */
protected function getIndexes(): array public function getIndexes(): array
{ {
return [$this->sourceIndex, $this->dependenciesIndex]; return [$this->sourceIndex, $this->dependenciesIndex];
} }

View File

@ -53,6 +53,11 @@ class Indexer
*/ */
private $documentLoader; private $documentLoader;
/**
* @var Options
*/
private $options;
/** /**
* @var \stdClasss * @var \stdClasss
*/ */
@ -63,6 +68,16 @@ class Indexer
*/ */
private $composerJson; private $composerJson;
/**
* @var bool
*/
private $hasCancellationSignal;
/**
* @var bool
*/
private $isIndexing;
/** /**
* @param FilesFinder $filesFinder * @param FilesFinder $filesFinder
* @param string $rootPath * @param string $rootPath
@ -93,6 +108,22 @@ class Indexer
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
$this->composerJson = $composerJson; $this->composerJson = $composerJson;
$this->hasCancellationSignal = false;
$this->isIndexing = false;
$this->options = new Options();
}
/**
* @param Options $options
*/
public function setOptions(Options $options)
{
$this->options = $options;
}
public function getOptions(): Options
{
return $this->options;
} }
/** /**
@ -103,13 +134,14 @@ class Indexer
public function index(): Promise public function index(): Promise
{ {
return coroutine(function () { return coroutine(function () {
$fileTypes = implode(',', $this->options->fileTypes);
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath); $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath);
$uris = yield $this->filesFinder->find($pattern); $uris = yield $this->filesFinder->find($pattern);
$count = count($uris); $count = count($uris);
$startTime = microtime(true); $startTime = microtime(true);
$this->client->window->logMessage(MessageType::INFO, "$count files total"); $this->client->window->logMessage(MessageType::INFO, "$count files total");
$this->isIndexing = true;
/** @var string[] */ /** @var string[] */
$source = []; $source = [];
@ -135,6 +167,7 @@ class Indexer
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references'); $this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
yield $this->indexFiles($source); yield $this->indexFiles($source);
$this->sourceIndex->setStaticComplete(); $this->sourceIndex->setStaticComplete();
// Dynamic references // Dynamic references
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references'); $this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
yield $this->indexFiles($source); yield $this->indexFiles($source);
@ -187,6 +220,7 @@ class Indexer
} }
} }
$this->isIndexing = false;
$duration = (int)(microtime(true) - $startTime); $duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024)); $mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage( $this->client->window->logMessage(
@ -196,6 +230,35 @@ class Indexer
}); });
} }
/**
* Return current indexing state
*
* @return bool
*/
public function isIndexing(): bool
{
return $this->isIndexing;
}
/**
* Cancel all running indexing processes
*
* @return Promise
*/
public function cancel(): Promise
{
return coroutine(function () {
$this->hasCancellationSignal = true;
while ($this->isIndexing()) {
yield timeout();
}
$this->hasCancellationSignal = false;
$this->client->window->logMessage(MessageType::INFO, 'Indexing project canceled');
});
}
/** /**
* @param array $files * @param array $files
* @return Promise * @return Promise
@ -204,6 +267,11 @@ class Indexer
{ {
return coroutine(function () use ($files) { return coroutine(function () use ($files) {
foreach ($files as $i => $uri) { foreach ($files as $i => $uri) {
// abort current running indexing
if ($this->hasCancellationSignal) {
return;
}
// Skip open documents // Skip open documents
if ($this->documentLoader->isOpen($uri)) { if ($this->documentLoader->isOpen($uri)) {
continue; continue;

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
ConfigurationItem,
ServerCapabilities, ServerCapabilities,
ClientCapabilities, ClientCapabilities,
TextDocumentSyncKind, TextDocumentSyncKind,
@ -15,7 +16,7 @@ use LanguageServer\Protocol\{
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache}; use LanguageServer\Cache\{Cache, FileSystemCache, ClientCache};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
@ -106,6 +107,26 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
protected $definitionResolver; protected $definitionResolver;
/**
* @var string|null
*/
protected $rootPath;
/**
* @var Cache
*/
protected $cache;
/**
* @var ClientCapabilities
*/
protected $clientCapabilities;
/**
* @var Indexer
*/
protected $indexer;
/** /**
* @param ProtocolReader $reader * @param ProtocolReader $reader
* @param ProtocolWriter $writer * @param ProtocolWriter $writer
@ -162,13 +183,18 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* *
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. * @param int|null $processId The process Id of the parent process that started the server.
* Is null if the process has not been started by another process.
* If the parent process is not alive then the server should exit
* (see exit notification) its process.
* @return Promise <InitializeResult> * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise public function initialize(
{ ClientCapabilities $capabilities,
string $rootPath = null,
int $processId = null
): Promise {
return coroutine(function () use ($capabilities, $rootPath, $processId) { return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {
$this->filesFinder = new ClientFilesFinder($this->client); $this->filesFinder = new ClientFilesFinder($this->client);
} else { } else {
@ -186,57 +212,64 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson);
$stubsIndex = StubsIndex::read(); $stubsIndex = StubsIndex::read();
$this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex);
$this->rootPath = $rootPath;
$this->clientCapabilities = $capabilities;
// The DefinitionResolver should look in stubs, the project source and dependencies // The DefinitionResolver should look in stubs, the project source and dependencies
$this->definitionResolver = new DefinitionResolver($this->globalIndex); $this->definitionResolver = new DefinitionResolver($this->globalIndex);
$this->documentLoader = new PhpDocumentLoader( $this->documentLoader = new PhpDocumentLoader(
$this->contentRetriever, $this->contentRetriever,
$this->projectIndex, $this->projectIndex,
$this->definitionResolver $this->definitionResolver
); );
if ($rootPath !== null) { if ($this->rootPath !== null) {
yield $this->beforeIndex($rootPath); yield $this->beforeIndex($this->rootPath);
// Find composer.json // Find composer.json
if ($this->composerJson === null) { if ($this->composerJson === null) {
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath)); $composerJsonFiles = yield $this->filesFinder->find(
Path::makeAbsolute('**/composer.json', $this->rootPath)
);
sortUrisLevelOrder($composerJsonFiles); sortUrisLevelOrder($composerJsonFiles);
if (!empty($composerJsonFiles)) { if (!empty($composerJsonFiles)) {
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0])); $this->composerJson = json_decode(
yield $this->contentRetriever->retrieve($composerJsonFiles[0])
);
} }
} }
// Find composer.lock // Find composer.lock
if ($this->composerLock === null) { if ($this->composerLock === null) {
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath)); $composerLockFiles = yield $this->filesFinder->find(
Path::makeAbsolute('**/composer.lock', $this->rootPath)
);
sortUrisLevelOrder($composerLockFiles); sortUrisLevelOrder($composerLockFiles);
if (!empty($composerLockFiles)) { if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0])); $this->composerLock = json_decode(
yield $this->contentRetriever->retrieve($composerLockFiles[0])
);
} }
} }
$cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache; $this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
// Index in background // Index in background
$indexer = new Indexer( $this->indexer = new Indexer(
$this->filesFinder, $this->filesFinder,
$rootPath, $this->rootPath,
$this->client, $this->client,
$cache, $this->cache,
$dependenciesIndex, $dependenciesIndex,
$sourceIndex, $sourceIndex,
$this->documentLoader, $this->documentLoader,
$this->composerLock, $this->composerLock,
$this->composerJson $this->composerJson
); );
$indexer->index()->otherwise('\\LanguageServer\\crash');
} }
if ($this->textDocument === null) { if ($this->textDocument === null) {
$this->textDocument = new Server\TextDocument( $this->textDocument = new Server\TextDocument(
$this->documentLoader, $this->documentLoader,
@ -247,12 +280,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->composerLock $this->composerLock
); );
} }
if ($this->workspace === null) { if ($this->workspace === null) {
$this->workspace = new Server\Workspace( $this->workspace = new Server\Workspace(
$this->client, $this->client,
$this->projectIndex, $this->projectIndex,
$dependenciesIndex, $dependenciesIndex,
$sourceIndex, $sourceIndex,
$this->indexer,
$this->composerLock, $this->composerLock,
$this->documentLoader, $this->documentLoader,
$this->composerJson $this->composerJson
@ -289,10 +324,46 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
}); });
} }
/**
* The initialized notification is sent from the client to the server after
* the client received the result of the initialize request but before the
* client is sending any other request or notification to the server.
*
* @return Promise
*/
public function initialized(): Promise
{
return coroutine(function () {
if (!$this->rootPath) {
return;
}
// request configuration if it is supported
// support comes with protocol version 3.6.0
if ($this->clientCapabilities->workspace->configuration) {
$configuration = yield $this->client->workspace->configuration([new ConfigurationItem('php')]);
$options = $this->mapper->map($configuration[0], new Options());
}
// depending on the implementation of the client
// the workspace/didChangeConfiguration can be invoked before
// the response from the workspace/configuration request is resolved
if ($this->indexer->isIndexing()) {
return;
}
if ($options) {
$this->indexer->setOptions($options);
}
$this->indexer->index()->otherwise('\\LanguageServer\\crash');
});
}
/** /**
* The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit * The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit
* (otherwise the response might not be delivered correctly to the client). There is a separate exit notification that * (otherwise the response might not be delivered correctly to the client). There is a separate exit notification
* asks the server to exit. * that asks the server to exit.
* *
* @return void * @return void
*/ */

50
src/Options.php Normal file
View File

@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
class Options
{
/**
* File types the indexer should process
*
* @var string[]
*/
public $fileTypes = ['.php'];
/**
* Validate/Filter input and set options for file types
*
* @param string[] $fileTypes List of file types
*/
public function setFileTypes(array $fileTypes)
{
$fileTypes = filter_var_array($fileTypes, FILTER_SANITIZE_STRING);
$fileTypes = filter_var($fileTypes, FILTER_CALLBACK, ['options' => [$this, 'filterFileTypes']]);
$fileTypes = array_filter($fileTypes, 'strlen');
$fileTypes = array_values($fileTypes);
$this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes;
}
/**
* Filter valid file type
*
* @param string $fileType The file type to filter
* @return string|bool If valid it returns the file type, otherwise false
*/
private function filterFileTypes(string $fileType)
{
$fileType = trim($fileType);
if (empty($fileType)) {
return $fileType;
}
if (substr($fileType, 0, 1) !== '.') {
return false;
}
return $fileType;
}
}

View File

@ -24,4 +24,9 @@ class ClientCapabilities
* @var bool|null * @var bool|null
*/ */
public $xcacheProvider; public $xcacheProvider;
/**
* @var WorkspaceClientCapabilities
*/
public $workspace;
} }

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class ConfigurationItem
{
/**
* The scope to get the configuration section for.
*
* @var string|null
*/
public $scopeUri;
/**
* The configuration section asked for.
*
* @var string|null
*/
public $section;
public function __construct(string $section = null, string $scopeUri = null)
{
$this->section = $section;
$this->scopeUri = $scopeUri;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace LanguageServer\Protocol;
class WorkspaceClientCapabilities
{
/**
* @var bool|null
*/
public $configuration;
}

View File

@ -3,11 +3,12 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, PhpDocumentLoader}; use LanguageServer\{Indexer, LanguageClient, Options, PhpDocumentLoader};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
FileChangeType, FileChangeType,
FileEvent, FileEvent,
MessageType,
SymbolInformation, SymbolInformation,
SymbolDescriptor, SymbolDescriptor,
ReferenceInformation, ReferenceInformation,
@ -45,6 +46,11 @@ class Workspace
*/ */
private $sourceIndex; private $sourceIndex;
/**
* @var Indexer
*/
private $indexer;
/** /**
* @var \stdClass * @var \stdClass
*/ */
@ -60,11 +66,20 @@ class Workspace
* @param ProjectIndex $projectIndex Index that is used to wait for full index completeness * @param ProjectIndex $projectIndex Index that is used to wait for full index completeness
* @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request * @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request * @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
* @param Indexer $indexer
* @param \stdClass $composerLock The parsed composer.lock of the project, if any * @param \stdClass $composerLock The parsed composer.lock of the project, if any
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents * @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
*/ */
public function __construct(LanguageClient $client, ProjectIndex $projectIndex, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null) public function __construct(
{ LanguageClient $client,
ProjectIndex $projectIndex,
DependenciesIndex $dependenciesIndex,
Index $sourceIndex,
Indexer $indexer,
\stdClass $composerLock = null,
PhpDocumentLoader $documentLoader,
\stdClass $composerJson = null
) {
$this->client = $client; $this->client = $client;
$this->sourceIndex = $sourceIndex; $this->sourceIndex = $sourceIndex;
$this->projectIndex = $projectIndex; $this->projectIndex = $projectIndex;
@ -72,10 +87,12 @@ class Workspace
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->composerJson = $composerJson; $this->composerJson = $composerJson;
$this->indexer = $indexer;
} }
/** /**
* The workspace symbol request is sent from the client to the server to list project-wide symbols matching the query string. * The workspace symbol request is sent from the client to the server to list
* project-wide symbols matching the query string.
* *
* @param string $query * @param string $query
* @return Promise <SymbolInformation[]> * @return Promise <SymbolInformation[]>
@ -98,7 +115,8 @@ class Workspace
} }
/** /**
* The watched files notification is sent from the client to the server when the client detects changes to files watched by the language client. * The watched files notification is sent from the client to the server when
* the client detects changes to files watched by the language client.
* *
* @param FileEvent[] $changes * @param FileEvent[] $changes
* @return void * @return void
@ -113,7 +131,8 @@ class Workspace
} }
/** /**
* The workspace references request is sent from the client to the server to locate project-wide references to a symbol given its description / metadata. * The workspace references request is sent from the client to the server to
* locate project-wide references to a symbol given its description / metadata.
* *
* @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for. * @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for.
* @param string[] $files An optional list of files to restrict the search to. * @param string[] $files An optional list of files to restrict the search to.
@ -174,4 +193,66 @@ class Workspace
} }
return $dependencyReferences; return $dependencyReferences;
} }
/**
* A notification sent from the client to the server to signal the change of configuration settings.
*
* @param mixed $settings Settings as JSON object structure with php as primary key
* @return Promise
*/
public function didChangeConfiguration($settings): Promise
{
return coroutine(function () use ($settings) {
if (!property_exists($settings, 'php') || $settings->php === new \stdClass()) {
return;
}
try {
$mapper = new \JsonMapper();
$options = $mapper->map($settings->php, new Options);
// handle options for indexer
$currentIndexerOptions = $this->indexer->getOptions();
$this->indexer->setOptions($options);
if ($this->hasIndexerOptionsChanged($currentIndexerOptions, $options)) {
if ($this->indexer->isIndexing()) {
yield $this->indexer->cancel();
}
$this->projectIndex->wipe();
yield $this->indexer->index();
}
} catch (\JsonMapper_Exception $exception) {
$this->client->window->showMessage(
MessageType::ERROR,
'Settings could not be applied. For more information see logs.'
);
$this->client->window->logMessage(MessageType::ERROR, $exception->getMessage());
}
});
}
/**
* Compare current options with new
*
* When the new options differ from the current, then we need start
* to reindex the project folder.
*
* @param Options $current
* @param Options $new
* @return bool
*/
private function hasIndexerOptionsChanged(Options $current, Options $new): bool
{
$properties = ['fileTypes'];
foreach ($properties as $property) {
if ($current->{$property} !== $new->{$property}) {
return true;
}
}
return false;
}
} }

View File

@ -5,6 +5,7 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer; use LanguageServer\LanguageServer;
use LanguageServer\Options;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
Message, Message,
ClientCapabilities, ClientCapabilities,
@ -55,19 +56,25 @@ class LanguageServerTest extends TestCase
$promise = new Promise; $promise = new Promise;
$input = new MockProtocolStream; $input = new MockProtocolStream;
$output = new MockProtocolStream; $output = new MockProtocolStream;
$output->on('message', function (Message $msg) use ($promise) { $output->on('message', function (Message $msg) use ($promise, $input) {
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->method === 'workspace/configuration') {
$result = new \stdClass();
$result->fileTypes = ['.php'];
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
} elseif ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
if ($msg->body->params->type === MessageType::ERROR) { if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
$promise->fulfill(); $promise->fulfill(true);
} }
} }
}); });
$server = new LanguageServer($input, $output); $server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities; $capabilities = new ClientCapabilities;
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid());
$promise->wait(); $server->initialized();
$this->assertTrue($promise->wait());
} }
public function testIndexingWithFilesAndContentRequests() public function testIndexingWithFilesAndContentRequests()
@ -80,7 +87,12 @@ class LanguageServerTest extends TestCase
$output = new MockProtocolStream; $output = new MockProtocolStream;
$run = 1; $run = 1;
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) { $output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) {
if ($msg->body->method === 'textDocument/xcontent') { if ($msg->body->method === 'workspace/configuration') {
$result = new \stdClass();
$result->fileTypes = ['.php'];
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
} elseif ($msg->body->method === 'textDocument/xcontent') {
// Document content requested // Document content requested
$contentCalled = true; $contentCalled = true;
$textDocumentItem = new TextDocumentItem; $textDocumentItem = new TextDocumentItem;
@ -114,9 +126,37 @@ class LanguageServerTest extends TestCase
$capabilities = new ClientCapabilities; $capabilities = new ClientCapabilities;
$capabilities->xfilesProvider = true; $capabilities->xfilesProvider = true;
$capabilities->xcontentProvider = true; $capabilities->xcontentProvider = true;
$server->initialize($capabilities, $rootPath, getmypid()); $server->initialize($capabilities, $rootPath, getmypid())->wait();
$server->initialized();
$promise->wait(); $promise->wait();
$this->assertTrue($filesCalled); $this->assertTrue($filesCalled);
$this->assertTrue($contentCalled); $this->assertTrue($contentCalled);
} }
public function testIndexingMultipleFileTypes()
{
$promise = new Promise;
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$output->on('message', function (Message $msg) use ($promise, $input) {
if ($msg->body->method === 'workspace/configuration') {
$result = new \stdClass();
$result->fileTypes = ['.php', '.inc'];
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
} elseif ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message));
} elseif (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
$promise->fulfill(true);
}
}
});
$server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities;
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid());
$server->initialized();
$this->assertTrue($promise->wait());
}
} }

28
tests/OptionsTest.php Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Options;
class OptionsTest extends TestCase
{
public function testFileTypesOption()
{
$expected = [
'.php',
'.valid'
];
$options = new Options;
$options->setFileTypes([
'.php',
false,
12345,
'.valid'
]);
$this->assertSame($expected, $options->fileTypes);
}
}

View File

@ -5,9 +5,7 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{ use LanguageServer\{Options, Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range}; use LanguageServer\Protocol\{Position, Location, Range};
@ -46,6 +44,7 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$options = new Options();
$sourceIndex = new Index; $sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex; $dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
@ -55,7 +54,7 @@ abstract class ServerTestCase extends TestCase
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader); $this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $this->documentLoader);
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));

View File

@ -0,0 +1,231 @@
<?php
/**
* Copyright (c) 2018 Jürgen Steitz
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
declare(strict_types=1);
namespace LanguageServer\Tests\Server\Workspace;
use Exception;
use LanguageServer\Cache\FileSystemCache;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\DefinitionResolver;
use LanguageServer\FilesFinder\FileSystemFilesFinder;
use LanguageServer\Index\DependenciesIndex;
use LanguageServer\Index\Index;
use LanguageServer\Index\ProjectIndex;
use LanguageServer\Indexer;
use LanguageServer\LanguageClient;
use LanguageServer\Options;
use LanguageServer\PhpDocumentLoader;
use LanguageServer\Protocol\Message;
use LanguageServer\Protocol\MessageType;
use LanguageServer\Server;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase;
use Sabre\Event\Promise;
class DidChangeConfigurationTest extends ServerTestCase
{
public function testFailsWithInvalidOptionsTypeOrFormat()
{
$promise = new Promise;
$sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$projectIndex->setComplete();
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
$filesFinder = new FileSystemFilesFinder;
$cache = new FileSystemCache;
$initialOptions = new Options;
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient($input, $output);
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
$indexer = new Indexer(
$filesFinder,
$rootPath,
$client,
$cache,
$dependenciesIndex,
$sourceIndex,
$documentLoader,
$initialOptions
);
$workspace = new Server\Workspace(
$client,
$projectIndex,
$dependenciesIndex,
$sourceIndex,
$initialOptions,
null,
$documentLoader
);
$output->on('message', function (Message $msg) use ($promise) {
if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) {
$hasMessage = strpos(
$msg->body->params->message,
'Settings could not be applied. For more information see logs.'
) !== false;
if ($msg->body->params->type === MessageType::ERROR && $hasMessage) {
$promise->fulfill(true);
}
if ($msg->body->params->type !== MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message));
}
}
});
$settings = new \stdClass();
$settings->php = new \stdClass();
$settings->php->fileTypes = 'not an array';
$workspace->didChangeConfiguration($settings);
$this->assertTrue($promise->wait());
}
public function testNoChangedOptions()
{
$promise = new Promise;
$sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$projectIndex->setComplete();
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
$filesFinder = new FileSystemFilesFinder;
$cache = new FileSystemCache;
$initialOptions = new Options;
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient($input, $output);
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
$indexer = new Indexer(
$filesFinder,
$rootPath,
$client,
$cache,
$dependenciesIndex,
$sourceIndex,
$documentLoader,
$initialOptions
);
$workspace = new Server\Workspace(
$client,
$projectIndex,
$dependenciesIndex,
$sourceIndex,
$initialOptions,
null,
$documentLoader
);
$output->on('message', function (Message $msg) use ($promise) {
$promise->reject(new Exception($msg->body->message));
});
$settings = new \stdClass();
$settings->php = new \stdClass();
$settings->php->fileTypes = ['.php'];
$this->expectException(\LogicException::class);
$workspace->didChangeConfiguration($settings);
$promise->wait();
}
public function testDetectsChangedOptions()
{
$promise = new Promise;
$sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$projectIndex->setComplete();
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
$filesFinder = new FileSystemFilesFinder;
$cache = new FileSystemCache;
$initialOptions = new Options;
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient($input, $output);
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
$indexer = new Indexer(
$filesFinder,
$rootPath,
$client,
$cache,
$dependenciesIndex,
$sourceIndex,
$documentLoader,
$initialOptions
);
$workspace = new Server\Workspace(
$client,
$projectIndex,
$dependenciesIndex,
$sourceIndex,
$initialOptions,
null,
$documentLoader
);
$output->on('message', function (Message $msg) use ($promise) {
if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) {
$hasMessage = strpos(
$msg->body->params->message,
'You must restart your editor for the changes to take effect.'
) !== false;
if ($msg->body->params->type === MessageType::INFO && $hasMessage) {
$promise->fulfill(true);
}
if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message));
}
}
});
$settings = new \stdClass();
$settings->php = new \stdClass();
$settings->php->fileTypes = ['.php', '.php5']; // default is only .php
$workspace->didChangeConfiguration($settings);
$this->assertTrue($promise->wait());
}
}

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\Workspace; namespace LanguageServer\Tests\Server\Workspace;
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\{DefinitionResolver, LanguageClient, PhpDocumentLoader, Server}; use LanguageServer\{DefinitionResolver, LanguageClient, Options, PhpDocumentLoader, Server};
use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex}; use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex};
use LanguageServer\Protocol\{FileChangeType, FileEvent, Message}; use LanguageServer\Protocol\{FileChangeType, FileEvent, Message};
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
@ -16,11 +16,12 @@ class DidChangeWatchedFilesTest extends ServerTestCase
{ {
public function testDeletingFileClearsAllDiagnostics() public function testDeletingFileClearsAllDiagnostics()
{ {
$options = new Options();
$client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream());
$projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex()); $projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex());
$definitionResolver = new DefinitionResolver($projectIndex); $definitionResolver = new DefinitionResolver($projectIndex);
$loader = new PhpDocumentLoader(new FileSystemContentRetriever(), $projectIndex, $definitionResolver); $loader = new PhpDocumentLoader(new FileSystemContentRetriever(), $projectIndex, $definitionResolver);
$workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $loader, null); $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $loader, null);
$fileEvent = new FileEvent('my uri', FileChangeType::DELETED); $fileEvent = new FileEvent('my uri', FileChangeType::DELETED);