diff --git a/fixtures/different_extension.inc b/fixtures/different_extension.inc new file mode 100644 index 0000000..b3d9bbc --- /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 5377c3a..33b86e5 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -147,4 +147,14 @@ abstract class AbstractAggregateIndex implements ReadableIndex } 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 9cb975e..2755ad7 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -222,4 +222,15 @@ class Index implements ReadableIndex, \Serializable '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 af980f8..bc03baf 100644 --- a/src/Index/ProjectIndex.php +++ b/src/Index/ProjectIndex.php @@ -35,7 +35,7 @@ class ProjectIndex extends AbstractAggregateIndex /** * @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 85d1787..93812b9 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 @@ class Indexer $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 @@ class Indexer 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 @@ class Indexer $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 @@ class Indexer } } + $this->isIndexing = false; $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $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 * @return Promise @@ -204,6 +267,11 @@ class Indexer { 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 46281f5..9548b7d 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer; use LanguageServer\Protocol\{ + ConfigurationItem, ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, @@ -15,7 +16,7 @@ use LanguageServer\Protocol\{ 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 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher * * @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 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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 @@ 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 - * (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 0000000..48ba540 --- /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 5228c7d..c1ca7ea 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 0000000..dff8b95 --- /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 0000000..6b11685 --- /dev/null +++ b/src/Protocol/WorkspaceClientCapabilities.php @@ -0,0 +1,11 @@ +client = $client; $this->sourceIndex = $sourceIndex; $this->projectIndex = $projectIndex; @@ -72,10 +87,12 @@ class Workspace $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 @@ 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 * @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 string[] $files An optional list of files to restrict the search to. @@ -174,4 +193,66 @@ class Workspace } 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 52963e6..df54103 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -5,6 +5,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; +use LanguageServer\Options; use LanguageServer\Protocol\{ Message, ClientCapabilities, @@ -55,19 +56,25 @@ class LanguageServerTest extends TestCase $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 @@ class LanguageServerTest extends TestCase $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 @@ class LanguageServerTest extends TestCase $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 0000000..45e35f6 --- /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 45d949f..27d64e0 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,9 +5,7 @@ namespace LanguageServer\Tests\Server; 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 @@ abstract class ServerTestCase extends TestCase $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 0000000..031852e --- /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 1074c58..d93985c 100644 --- a/tests/Server/Workspace/DidChangeWatchedFilesTest.php +++ b/tests/Server/Workspace/DidChangeWatchedFilesTest.php @@ -4,7 +4,7 @@ declare(strict_types = 1); 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);