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/Indexer.php b/src/Indexer.php index e411054..253c7d7 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -68,6 +68,16 @@ class Indexer */ private $composerJson; + /** + * @var bool + */ + private $hasCancellationSignal; + + /** + * @var bool + */ + private $isIndexing; + /** * @param FilesFinder $filesFinder * @param string $rootPath @@ -101,6 +111,8 @@ class Indexer $this->options = $options; $this->composerLock = $composerLock; $this->composerJson = $composerJson; + $this->hasCancellationSignal = false; + $this->isIndexing = false; } /** @@ -118,6 +130,7 @@ class Indexer $count = count($uris); $startTime = microtime(true); $this->client->window->logMessage(MessageType::INFO, "$count files total"); + $this->isIndexing = true; /** @var string[] */ $source = []; @@ -195,6 +208,7 @@ class Indexer } } + $this->isIndexing = false; $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage( @@ -204,6 +218,34 @@ 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; + }); + } + /** * @param array $files * @return Promise @@ -212,6 +254,10 @@ class Indexer { return coroutine(function () use ($files) { foreach ($files as $i => $uri) { + if ($this->hasCancellationSignal) { + return; + } + // Skip open documents if ($this->documentLoader->isOpen($uri)) { continue; diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 00822d7..29cfc4c 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -329,6 +329,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex, $sourceIndex, $options, + $indexer, $this->composerLock, $this->documentLoader, $this->composerJson diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 548197b..8eee501 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -3,11 +3,12 @@ declare(strict_types = 1); namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, PhpDocumentLoader}; +use LanguageServer\{Indexer, LanguageClient, Options, PhpDocumentLoader}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Protocol\{ FileChangeType, FileEvent, + MessageType, SymbolInformation, SymbolDescriptor, ReferenceInformation, @@ -45,6 +46,16 @@ class Workspace */ private $sourceIndex; + /** + * @var Options + */ + private $options; + + /** + * @var Indexer + */ + private $indexer; + /** * @var \stdClass */ @@ -60,11 +71,22 @@ class Workspace * @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 $sourceIndex Index that is used on a workspace/xreferences request + * @param Options $options Initialization options that are used on a workspace/didChangeConfiguration + * @param Indexer $indexer * @param \stdClass $composerLock The parsed composer.lock of the project, if any * @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, + Options $options, + Indexer $indexer, + \stdClass $composerLock = null, + PhpDocumentLoader $documentLoader, + \stdClass $composerJson = null + ) { $this->client = $client; $this->sourceIndex = $sourceIndex; $this->projectIndex = $projectIndex; @@ -72,10 +94,13 @@ class Workspace $this->composerLock = $composerLock; $this->documentLoader = $documentLoader; $this->composerJson = $composerJson; + $this->options = $options; + $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 +123,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 +139,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 +201,47 @@ class Workspace } return $dependencyReferences; } + + /** + * A notification sent from the client to the server to signal the change of configuration settings. + * + * @param \stdClass $settings Settings as JSON object structure with php as primary key + * @return Promise + */ + public function didChangeConfiguration(\stdClass $settings): Promise + { + xdebug_break(); + return coroutine(function () use ($settings) { + try { + xdebug_break(); + $mapper = new \JsonMapper(); + $settings = $mapper->map($settings->php, new Options); + + if ($this->options == $settings) { + return; + } + + // @TODO: get changed settings and apply them + // @TODO: check settings that affect the indexer + + if ($this->indexer->isIndexing()) { + yield $this->indexer->cancel(); + } + + $this->projectIndex->wipe(); + $this->indexer->index(); + + $this->client->window->showMessage( + MessageType::INFO, + 'Reindexing with new settings.' + ); + } 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()); + } + }); + } } 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);