diff --git a/fixtures/different_extension.inc b/fixtures/different_extension.inc new file mode 100644 index 00000000..b3d9bbc7 --- /dev/null +++ b/fixtures/different_extension.inc @@ -0,0 +1 @@ +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 + */ + public function configuration(array $items): Promise + { + return $this->handler->request( + 'workspace/configuration', + ['items' => $items] + ); + } } diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 5377c3a4..33b86e55 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -147,4 +147,14 @@ public function getReferenceUris(string $fqn): array } return $refs; } + + /** + * Wipe all indexes for a reindex + */ + public function wipe() + { + foreach ($this->getIndexes() as $index) { + $index->wipe(); + } + } } diff --git a/src/Index/Index.php b/src/Index/Index.php index 9cb975e5..2755ad70 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -222,4 +222,15 @@ public function serialize() 'staticComplete' => $this->staticComplete ]); } + + /** + * Clear indexed references and definitions + */ + public function wipe() + { + $this->definitions = []; + $this->references = []; + $this->complete = false; + $this->staticComplete = false; + } } diff --git a/src/Index/ProjectIndex.php b/src/Index/ProjectIndex.php index af980f8f..bc03baf0 100644 --- a/src/Index/ProjectIndex.php +++ b/src/Index/ProjectIndex.php @@ -35,7 +35,7 @@ public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesI /** * @return ReadableIndex[] */ - protected function getIndexes(): array + public function getIndexes(): array { return [$this->sourceIndex, $this->dependenciesIndex]; } diff --git a/src/Indexer.php b/src/Indexer.php index 85d17872..93812b93 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -53,6 +53,11 @@ class Indexer */ private $documentLoader; + /** + * @var Options + */ + private $options; + /** * @var \stdClasss */ @@ -63,6 +68,16 @@ class Indexer */ private $composerJson; + /** + * @var bool + */ + private $hasCancellationSignal; + + /** + * @var bool + */ + private $isIndexing; + /** * @param FilesFinder $filesFinder * @param string $rootPath @@ -93,6 +108,22 @@ public function __construct( $this->documentLoader = $documentLoader; $this->composerLock = $composerLock; $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 @@ public function __construct( public function index(): Promise { return coroutine(function () { - - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); + $fileTypes = implode(',', $this->options->fileTypes); + $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath); $uris = yield $this->filesFinder->find($pattern); $count = count($uris); $startTime = microtime(true); $this->client->window->logMessage(MessageType::INFO, "$count files total"); + $this->isIndexing = true; /** @var string[] */ $source = []; @@ -135,6 +167,7 @@ public function index(): Promise $this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references'); yield $this->indexFiles($source); $this->sourceIndex->setStaticComplete(); + // Dynamic references $this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references'); yield $this->indexFiles($source); @@ -187,6 +220,7 @@ public function index(): Promise } } + $this->isIndexing = false; $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage( @@ -196,6 +230,35 @@ public function index(): Promise }); } + /** + * 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 * @return Promise @@ -204,6 +267,11 @@ private function indexFiles(array $files): Promise { return coroutine(function () use ($files) { foreach ($files as $i => $uri) { + // abort current running indexing + if ($this->hasCancellationSignal) { + return; + } + // Skip open documents if ($this->documentLoader->isOpen($uri)) { continue; diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 46281f51..9548b7d5 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -4,6 +4,7 @@ namespace LanguageServer; use LanguageServer\Protocol\{ + ConfigurationItem, ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, @@ -15,7 +16,7 @@ use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; -use LanguageServer\Cache\{FileSystemCache, ClientCache}; +use LanguageServer\Cache\{Cache, FileSystemCache, ClientCache}; use AdvancedJsonRpc; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -106,6 +107,26 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ protected $definitionResolver; + /** + * @var string|null + */ + protected $rootPath; + + /** + * @var Cache + */ + protected $cache; + + /** + * @var ClientCapabilities + */ + protected $clientCapabilities; + + /** + * @var Indexer + */ + protected $indexer; + /** * @param ProtocolReader $reader * @param ProtocolWriter $writer @@ -162,13 +183,18 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) * * @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 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 */ - 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) { - if ($capabilities->xfilesProvider) { $this->filesFinder = new ClientFilesFinder($this->client); } else { @@ -186,57 +212,64 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $this->rootPath = $rootPath; + $this->clientCapabilities = $capabilities; // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); - $this->documentLoader = new PhpDocumentLoader( $this->contentRetriever, $this->projectIndex, $this->definitionResolver ); - if ($rootPath !== null) { - yield $this->beforeIndex($rootPath); + if ($this->rootPath !== null) { + yield $this->beforeIndex($this->rootPath); // Find composer.json 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); 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 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); 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 - $indexer = new Indexer( + $this->indexer = new Indexer( $this->filesFinder, - $rootPath, + $this->rootPath, $this->client, - $cache, + $this->cache, $dependenciesIndex, $sourceIndex, $this->documentLoader, $this->composerLock, $this->composerJson ); - $indexer->index()->otherwise('\\LanguageServer\\crash'); } - if ($this->textDocument === null) { $this->textDocument = new Server\TextDocument( $this->documentLoader, @@ -247,12 +280,14 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->composerLock ); } + if ($this->workspace === null) { $this->workspace = new Server\Workspace( $this->client, $this->projectIndex, $dependenciesIndex, $sourceIndex, + $this->indexer, $this->composerLock, $this->documentLoader, $this->composerJson @@ -289,10 +324,46 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = }); } + /** + * 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 - * (otherwise the response might not be delivered correctly to the client). There is a separate exit notification that - * asks the server to exit. + * (otherwise the response might not be delivered correctly to the client). There is a separate exit notification + * that asks the server to exit. * * @return void */ diff --git a/src/Options.php b/src/Options.php new file mode 100644 index 00000000..48ba540b --- /dev/null +++ b/src/Options.php @@ -0,0 +1,50 @@ + [$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; + } +} diff --git a/src/Protocol/ClientCapabilities.php b/src/Protocol/ClientCapabilities.php index 5228c7d9..c1ca7ea6 100644 --- a/src/Protocol/ClientCapabilities.php +++ b/src/Protocol/ClientCapabilities.php @@ -24,4 +24,9 @@ class ClientCapabilities * @var bool|null */ public $xcacheProvider; + + /** + * @var WorkspaceClientCapabilities + */ + public $workspace; } diff --git a/src/Protocol/ConfigurationItem.php b/src/Protocol/ConfigurationItem.php new file mode 100644 index 00000000..dff8b95f --- /dev/null +++ b/src/Protocol/ConfigurationItem.php @@ -0,0 +1,27 @@ +section = $section; + $this->scopeUri = $scopeUri; + } +} diff --git a/src/Protocol/WorkspaceClientCapabilities.php b/src/Protocol/WorkspaceClientCapabilities.php new file mode 100644 index 00000000..6b116859 --- /dev/null +++ b/src/Protocol/WorkspaceClientCapabilities.php @@ -0,0 +1,11 @@ +client = $client; $this->sourceIndex = $sourceIndex; $this->projectIndex = $projectIndex; @@ -72,10 +87,12 @@ public function __construct(LanguageClient $client, ProjectIndex $projectIndex, $this->composerLock = $composerLock; $this->documentLoader = $documentLoader; $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 * @return Promise @@ -98,7 +115,8 @@ public function symbol(string $query): Promise } /** - * 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 * @return void @@ -113,7 +131,8 @@ public function didChangeWatchedFiles(array $changes) } /** - * 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 string[] $files An optional list of files to restrict the search to. @@ -174,4 +193,66 @@ public function xdependencies(): array } 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; + } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 52963e6c..df541033 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; +use LanguageServer\Options; use LanguageServer\Protocol\{ Message, ClientCapabilities, @@ -55,19 +56,25 @@ public function testIndexingWithDirectFileAccess() $promise = new Promise; $input = new MockProtocolStream; $output = new MockProtocolStream; - $output->on('message', function (Message $msg) use ($promise) { - if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { + $output->on('message', function (Message $msg) use ($promise, $input) { + 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) { $promise->reject(new Exception($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); $capabilities = new ClientCapabilities; $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); - $promise->wait(); + $server->initialized(); + $this->assertTrue($promise->wait()); } public function testIndexingWithFilesAndContentRequests() @@ -80,7 +87,12 @@ public function testIndexingWithFilesAndContentRequests() $output = new MockProtocolStream; $run = 1; $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 $contentCalled = true; $textDocumentItem = new TextDocumentItem; @@ -114,9 +126,37 @@ public function testIndexingWithFilesAndContentRequests() $capabilities = new ClientCapabilities; $capabilities->xfilesProvider = true; $capabilities->xcontentProvider = true; - $server->initialize($capabilities, $rootPath, getmypid()); + $server->initialize($capabilities, $rootPath, getmypid())->wait(); + $server->initialized(); $promise->wait(); $this->assertTrue($filesCalled); $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()); + } } diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php new file mode 100644 index 00000000..45e35f6d --- /dev/null +++ b/tests/OptionsTest.php @@ -0,0 +1,28 @@ +setFileTypes([ + '.php', + false, + 12345, + '.valid' + ]); + + $this->assertSame($expected, $options->fileTypes); + } +} diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 45d949ff..27d64e03 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,9 +5,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, DefinitionResolver -}; +use LanguageServer\{Options, Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range}; @@ -46,6 +44,7 @@ abstract class ServerTestCase extends TestCase public function setUp() { + $options = new Options(); $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); @@ -55,7 +54,7 @@ public function setUp() $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $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')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); diff --git a/tests/Server/Workspace/DidChangeConfigurationTest.php b/tests/Server/Workspace/DidChangeConfigurationTest.php new file mode 100644 index 00000000..031852ed --- /dev/null +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -0,0 +1,231 @@ +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()); + } +} diff --git a/tests/Server/Workspace/DidChangeWatchedFilesTest.php b/tests/Server/Workspace/DidChangeWatchedFilesTest.php index 1074c583..d93985c6 100644 --- a/tests/Server/Workspace/DidChangeWatchedFilesTest.php +++ b/tests/Server/Workspace/DidChangeWatchedFilesTest.php @@ -4,7 +4,7 @@ namespace LanguageServer\Tests\Server\Workspace; 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\Protocol\{FileChangeType, FileEvent, Message}; use LanguageServer\Tests\MockProtocolStream; @@ -16,11 +16,12 @@ class DidChangeWatchedFilesTest extends ServerTestCase { public function testDeletingFileClearsAllDiagnostics() { + $options = new Options(); $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); $projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex()); $definitionResolver = new DefinitionResolver($projectIndex); $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);