From cdb5b566136dd65e51d5061a272cdd908207cd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 01:18:16 +0100 Subject: [PATCH 01/29] Add support to index multiple file extensions Will take the options sent by the client. Option: php.intellisense.fileTypes = [".php"] --- src/Indexer.php | 30 +++++++++++++++++++----------- src/LanguageServer.php | 9 ++++++--- src/Options.php | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 src/Options.php diff --git a/src/Indexer.php b/src/Indexer.php index 34ad618..3529e19 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -65,14 +65,20 @@ class Indexer private $composerJson; /** - * @param FilesFinder $filesFinder - * @param string $rootPath - * @param LanguageClient $client - * @param Cache $cache - * @param DependenciesIndex $dependenciesIndex - * @param Index $sourceIndex - * @param PhpDocumentLoader $documentLoader - * @param \stdClass|null $composerLock + * @var Options + */ + private $options; + + /** + * @param FilesFinder $filesFinder + * @param string $rootPath + * @param LanguageClient $client + * @param Cache $cache + * @param DependenciesIndex $dependenciesIndex + * @param Index $sourceIndex + * @param PhpDocumentLoader $documentLoader + * @param \stdClass|null $composerLock + * @param IndexerOptions|null $options */ public function __construct( FilesFinder $filesFinder, @@ -83,7 +89,8 @@ class Indexer Index $sourceIndex, PhpDocumentLoader $documentLoader, \stdClass $composerLock = null, - \stdClass $composerJson = null + \stdClass $composerJson = null, + Options $options = null ) { $this->filesFinder = $filesFinder; $this->rootPath = $rootPath; @@ -94,6 +101,7 @@ class Indexer $this->documentLoader = $documentLoader; $this->composerLock = $composerLock; $this->composerJson = $composerJson; + $this->options = $options; } /** @@ -104,8 +112,8 @@ 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); diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 3c999f2..12ee173 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -167,11 +167,12 @@ 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 mixed $initializationOptions The options send from client to initialize the server * @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, $initializationOptions = null): Promise { - return coroutine(function () use ($capabilities, $rootPath, $processId) { + return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) { if ($capabilities->xfilesProvider) { $this->filesFinder = new ClientFilesFinder($this->client); @@ -190,6 +191,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $options = new Options($initializationOptions); // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); @@ -235,7 +237,8 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $sourceIndex, $this->documentLoader, $this->composerLock, - $this->composerJson + $this->composerJson, + $options ); $indexer->index()->otherwise('\\LanguageServer\\crash'); } diff --git a/src/Options.php b/src/Options.php new file mode 100644 index 0000000..3a4842e --- /dev/null +++ b/src/Options.php @@ -0,0 +1,37 @@ +fileTypes = $options->fileTypes ?? $this->normalizeFileTypes($this->fileTypes); + } + + private function normalizeFileTypes(array $fileTypes): array + { + return array_map(function (string $fileType) { + if (substr($fileType, 0, 1) !== '.') { + $fileType = '.' . $fileType; + } + + return $fileType; + }, $fileTypes); + } +} From 5f096c4bf782dbb4d72f399c3eafb1abd9fbb5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 01:18:52 +0100 Subject: [PATCH 02/29] Add test for indexing multiple file types --- fixtures/different_extension.inc | 1 + tests/LanguageServerTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 fixtures/different_extension.inc 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 @@ +assertTrue($filesCalled); $this->assertTrue($contentCalled); } + + public function testIndexingMultipleFileTypes() + { + $promise = new Promise; + $input = new MockProtocolStream; + $output = new MockProtocolStream; + $options = (object)[ + 'fileTypes' => [ + '.php', + '.inc' + ] + ]; + + $output->on('message', function (Message $msg) use ($promise, &$foundFiles) { + if ($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 (strpos($msg->body->params->message, 'All 27 PHP files parsed') !== false) { + $promise->fulfill(); + } + } + }); + $server = new LanguageServer($input, $output); + $capabilities = new ClientCapabilities; + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), $options); + $promise->wait(); + } } From 7dc44776f7c93015054bd8e1f115c902c9acf179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 09:37:50 +0100 Subject: [PATCH 03/29] Fix wrong phpDoc type --- src/Indexer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Indexer.php b/src/Indexer.php index 3529e19..4cd85d3 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -78,7 +78,7 @@ class Indexer * @param Index $sourceIndex * @param PhpDocumentLoader $documentLoader * @param \stdClass|null $composerLock - * @param IndexerOptions|null $options + * @param Options|null $options */ public function __construct( FilesFinder $filesFinder, From f7175bc195b1895fca908d99033c4d68f076d87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 10:38:55 +0100 Subject: [PATCH 04/29] Filter invalid file types and use default list as fallback --- src/Indexer.php | 2 +- src/Options.php | 60 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/Indexer.php b/src/Indexer.php index 4cd85d3..c341dd2 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -112,7 +112,7 @@ class Indexer public function index(): Promise { return coroutine(function () { - $fileTypes = implode(',', $this->options->fileTypes); + $fileTypes = implode(',', $this->options->getFileTypes()); $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath); $uris = yield $this->filesFinder->find($pattern); diff --git a/src/Options.php b/src/Options.php index 3a4842e..d7e7884 100644 --- a/src/Options.php +++ b/src/Options.php @@ -9,29 +9,67 @@ class Options * * @var array */ - public $fileTypes = [".php"]; + private $fileTypes = [".php"]; /** - * @param \stdClass|null $options + * @param \Traversable|\stdClass|array|null $options */ - public function __construct(\stdClass $options = null) + public function __construct($options = null) { // Do nothing when the $options parameter is not an object - if (!is_object($options)) { + if (!is_object($options) && !is_array($options) && (!$options instanceof \Traversable)) { return; } - $this->fileTypes = $options->fileTypes ?? $this->normalizeFileTypes($this->fileTypes); + foreach ($options as $option => $value) { + $method = 'set' . ucfirst($option); + + call_user_func([$this, $method], $value); + } } - private function normalizeFileTypes(array $fileTypes): array + /** + * Validate and set options for file types + * + * @param array $fileTypes List of file types + */ + public function setFileTypes(array $fileTypes) { - return array_map(function (string $fileType) { - if (substr($fileType, 0, 1) !== '.') { - $fileType = '.' . $fileType; - } + $fileTypes = filter_var_array($fileTypes, FILTER_SANITIZE_STRING); + $fileTypes = filter_var($fileTypes, FILTER_CALLBACK, ['options' => [$this, 'filterFileTypes']]); + $fileTypes = array_filter($fileTypes); + $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; + } + + /** + * Get list of registered file types + * + * @return array + */ + public function getFileTypes(): array + { + return $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; - }, $fileTypes); + } + + if (substr($fileType, 0, 1) !== '.') { + return false; + } + + return $fileType; } } From 94336941bd585fb90df749f8b27183236603bff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 19:30:34 +0100 Subject: [PATCH 05/29] Let JsonMapper intialize the options To sanitize the file type option, we provide a setter method for the property that will be called by the JsonMapper. --- src/Indexer.php | 2 +- src/LanguageServer.php | 7 +++---- src/Options.php | 38 ++++++-------------------------------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/Indexer.php b/src/Indexer.php index c341dd2..4cd85d3 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -112,7 +112,7 @@ class Indexer public function index(): Promise { return coroutine(function () { - $fileTypes = implode(',', $this->options->getFileTypes()); + $fileTypes = implode(',', $this->options->fileTypes); $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath); $uris = yield $this->filesFinder->find($pattern); diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 12ee173..7e5074b 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -167,10 +167,10 @@ 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 mixed $initializationOptions The options send from client to initialize the server + * @param Options $initializationOptions The options send from client to initialize the server * @return Promise */ - public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, $initializationOptions = null): Promise + public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, Options $initializationOptions = null): Promise { return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) { @@ -191,7 +191,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); - $options = new Options($initializationOptions); // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); @@ -238,7 +237,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->documentLoader, $this->composerLock, $this->composerJson, - $options + $initializationOptions ); $indexer->index()->otherwise('\\LanguageServer\\crash'); } diff --git a/src/Options.php b/src/Options.php index d7e7884..99f1aab 100644 --- a/src/Options.php +++ b/src/Options.php @@ -7,51 +7,25 @@ class Options /** * Filetypes the indexer should process * - * @var array + * @var string[] */ - private $fileTypes = [".php"]; + public $fileTypes = ['.php']; /** - * @param \Traversable|\stdClass|array|null $options - */ - public function __construct($options = null) - { - // Do nothing when the $options parameter is not an object - if (!is_object($options) && !is_array($options) && (!$options instanceof \Traversable)) { - return; - } - - foreach ($options as $option => $value) { - $method = 'set' . ucfirst($option); - - call_user_func([$this, $method], $value); - } - } - - /** - * Validate and set options for file types + * Validate/Filter input and set options for file types * * @param array $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); + $fileTypes = filter_var($fileTypes, FILTER_CALLBACK, ['options' => [$this, 'filterFileTypes']]); // validate file type format + $fileTypes = array_filter($fileTypes, 'strlen'); // filter empty items + $fileTypes = array_values($fileTypes); //rebase indexes $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; } - /** - * Get list of registered file types - * - * @return array - */ - public function getFileTypes(): array - { - return $this->fileTypes; - } - /** * Filter valid file type * From 39cfbda77b59aaa67833632b11ffc011e2d10f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 19:30:56 +0100 Subject: [PATCH 06/29] Add test for fileTypes option --- tests/OptionsTest.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/OptionsTest.php diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php new file mode 100644 index 0000000..d47afcf --- /dev/null +++ b/tests/OptionsTest.php @@ -0,0 +1,26 @@ +setFileTypes([ + '.php', + false, + 12345, + '.valid' + ]); + + $this->assertSame($expected, $options->fileTypes); + } +} From 3c33e7f46602c71c81d6bbbc557653db76a1363b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 20:05:21 +0100 Subject: [PATCH 07/29] Initialize options with default values when not provided by client --- src/LanguageServer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 7e5074b..d3b1c5c 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -191,6 +191,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $initializationOptions = $initializationOptions ?? new Options; // The DefinitionResolver should look in stubs, the project source and dependencies $this->definitionResolver = new DefinitionResolver($this->globalIndex); From d2e5048ec8cf8d3b10178d4caa4ce68f6cc3df65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 20:06:24 +0100 Subject: [PATCH 08/29] Update testIndexingMultipleFileTypes --- tests/LanguageServerTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 0c0740c..9b16902 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, @@ -123,12 +124,12 @@ class LanguageServerTest extends TestCase $promise = new Promise; $input = new MockProtocolStream; $output = new MockProtocolStream; - $options = (object)[ - 'fileTypes' => [ - '.php', - '.inc' - ] - ]; + $options = new Options; + + $options->setFileTypes([ + '.php', + '.inc' + ]); $output->on('message', function (Message $msg) use ($promise, &$foundFiles) { if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { From b9d0d1bfa7fbca0f1ac5be5a5cf7b054d24dabc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 18 Feb 2017 20:27:35 +0100 Subject: [PATCH 09/29] Add missing namespace in OptionsTest --- tests/OptionsTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index d47afcf..f2251d8 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -1,6 +1,8 @@ Date: Sat, 18 Feb 2017 20:33:03 +0100 Subject: [PATCH 10/29] Fix wrong classname for options test --- tests/OptionsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index f2251d8..45e35f6 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\Options; -class LanguageServerTest extends TestCase +class OptionsTest extends TestCase { public function testFileTypesOption() { From 1e319c7215c85e3e8e0f0546b5b68fbdbc7a4528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Fri, 24 Feb 2017 23:37:02 +0100 Subject: [PATCH 11/29] Wipe index when on configuration change --- src/Index/AbstractAggregateIndex.php | 13 +++++++++++++ src/Index/Index.php | 10 ++++++++++ src/Server/Workspace.php | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 5377c3a..39c1a1d 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -42,6 +42,9 @@ abstract class AbstractAggregateIndex implements ReadableIndex $index->on('definition-added', function () { $this->emit('definition-added'); }); + $index->on('wipe', function() { + $this->emit('wipe'); + }); } /** @@ -147,4 +150,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 5c24813..e912087 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -211,4 +211,14 @@ class Index implements ReadableIndex, \Serializable 'staticComplete' => $this->staticComplete ]); } + + public function wipe() + { + $this->definitions = []; + $this->references = []; + $this->complete = false; + $this->staticComplete = false; + + $this->emit('wipe'); + } } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index b94618c..3a10b78 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -170,4 +170,9 @@ class Workspace } return $dependencyReferences; } + + public function didChangeConfiguration($settings = null) + { + $this->index->wipe(); + } } From 58c82e6dc914a786603f845a33a89ce3afa1bab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Thu, 2 Mar 2017 23:11:14 +0100 Subject: [PATCH 12/29] Add list of valid indexer options --- src/Options.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Options.php b/src/Options.php index 99f1aab..9f61edb 100644 --- a/src/Options.php +++ b/src/Options.php @@ -11,6 +11,11 @@ class Options */ public $fileTypes = ['.php']; + /** + * List of options that affect the indexer + */ + private $indexerOptions = ['fileTypes']; + /** * Validate/Filter input and set options for file types * @@ -26,6 +31,16 @@ class Options $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; } + /** + * Get list with options that affect the indexer + * + * @return array + */ + public function getIndexerOptions(): array + { + return $this->indexerOptions; + } + /** * Filter valid file type * From 44a942e714e8cd0cdf1ffc70c4c9fc3c1386de0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Thu, 2 Mar 2017 23:11:24 +0100 Subject: [PATCH 13/29] Implement didChangeConfiguration event --- src/Server/Workspace.php | 57 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 3a10b78..fb3807d 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, Project, PhpDocumentLoader}; +use LanguageServer\{LanguageClient, Project, PhpDocumentLoader, Options, Indexer}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Protocol\{SymbolInformation, SymbolDescriptor, ReferenceInformation, DependencyReference, Location}; use Sabre\Event\Promise; @@ -32,6 +32,16 @@ class Workspace */ private $sourceIndex; + /** + * @var Options + */ + private $options; + + /** + * @var Indexer + */ + private $indexer; + /** * @var \stdClass */ @@ -48,8 +58,10 @@ class Workspace * @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request * @param \stdClass $composerLock The parsed composer.lock of the project, if any * @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents + * @param Indexer $indexer + * @param Options $options */ - public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null) + public function __construct(ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null, Indexer $indexer = null, Options $options = null) { $this->sourceIndex = $sourceIndex; $this->index = $index; @@ -57,6 +69,8 @@ class Workspace $this->composerLock = $composerLock; $this->documentLoader = $documentLoader; $this->composerJson = $composerJson; + $this->indexer = $indexer; + $this->options = $options; } /** @@ -171,8 +185,43 @@ class Workspace return $dependencyReferences; } - public function didChangeConfiguration($settings = null) + /** + * @param Options|null $settings + */ + public function didChangeConfiguration(Options $settings = null) { - $this->index->wipe(); + if ($settings === null) { + return; + } + + $changedOptions = $this->getChangedOptions($settings); + $this->options = $settings; + + if (!empty(array_intersect($changedOptions, $this->options->getIndexerOptions()))) { + // check list of options that changed since last time against the list of valid indexer options + + // start wiping from the main index + $this->index->wipe(); + + // check for existing indexer and start indexing + if ($this->indexer) { + $this->indexer->index()->otherwise('\\LanguageServer\\crash'); + } + } + } + + /** + * Get a list with all options that changed since last time + * + * @param Options $settings + * @return array List with changed options + */ + private function getChangedOptions(Options $settings): array + { + // squash nested array for comparing changed options + $old = array_map('json_encode', get_object_vars($this->options)); + $new = array_map('json_encode', get_object_vars($settings)); + + return array_keys(array_diff($old, $new)); } } From 940eb9787d98204887210d0b18016235f841b4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Thu, 2 Mar 2017 23:11:38 +0100 Subject: [PATCH 14/29] Pass options and indexer to workspace --- src/LanguageServer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LanguageServer.php b/src/LanguageServer.php index d3b1c5c..62b8400 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -261,7 +261,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $sourceIndex, $this->composerLock, $this->documentLoader, - $this->composerJson + $this->composerJson, + $indexer, + $initializationOptions ); } From 5b1b6bfabed6c1681cf7a5091238186add41ac3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Thu, 2 Mar 2017 23:12:19 +0100 Subject: [PATCH 15/29] Add tests --- tests/Server/ServerTestCase.php | 30 +++++--- .../Workspace/DidChangeConfigurationTest.php | 72 +++++++++++++++++++ 2 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests/Server/Workspace/DidChangeConfigurationTest.php diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 679191f..1a45c64 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,10 +5,12 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver, Options, Indexer}; use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; +use LanguageServer\FilesFinder\FileSystemFilesFinder; +use LanguageServer\Cache\FileSystemCache; use function LanguageServer\pathToUri; use Sabre\Event\Promise; @@ -29,6 +31,10 @@ abstract class ServerTestCase extends TestCase */ protected $documentLoader; + protected $projectIndex; + protected $input; + protected $output; + /** * Map from FQN to Location of definition * @@ -47,14 +53,22 @@ abstract class ServerTestCase extends TestCase { $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; - $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); - $projectIndex->setComplete(); + $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $this->projectIndex->setComplete(); - $definitionResolver = new DefinitionResolver($projectIndex); - $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($projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader); + $rootPath = realpath(__DIR__ . '/../../fixtures/'); + $options = new Options; + $filesFinder = new FileSystemFilesFinder; + $cache = new FileSystemCache; + + $this->input = new MockProtocolStream; + $this->output = new MockProtocolStream; + $definitionResolver = new DefinitionResolver($this->projectIndex); + $client = new LanguageClient($this->input, $this->output); + $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $this->projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $this->projectIndex); + $indexer = new Indexer($filesFinder, $rootPath, $client, $cache, $dependenciesIndex, $sourceIndex, $this->documentLoader, null, null, $options); + $this->workspace = new Server\Workspace($this->projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader, null, $indexer, $options); $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..18970e9 --- /dev/null +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -0,0 +1,72 @@ +projectIndex->on('wipe', function() use ($promise) { + $promise->fulfill(); + }); + + $options = new Options; + $options->fileTypes = [ + '.inc' + ]; + + $this->workspace->didChangeConfiguration($options); + $promise->wait(); + } + + public function testReindexingAfterWipe() + { + $promise = new Promise; + + $this->output->on('message', function (Message $msg) use ($promise) { + if ($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 (strpos($msg->body->params->message, 'All 0 PHP files parsed') !== false) { + $promise->fulfill(); + } + } + }); + + $options = new Options; + $options->fileTypes = [ + '.inc' + ]; + + $this->workspace->didChangeConfiguration($options); + $promise->wait(); + } + + public function testGetChangedOptions() + { + } +} From 1e73d08033df2bd27d830cb4b848b7ee32c07936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 09:03:24 +0100 Subject: [PATCH 16/29] Improve gettting changed options --- src/Server/Workspace.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 1f35134..7f76977 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -248,10 +248,14 @@ class Workspace */ private function getChangedOptions(Options $settings): array { - // squash nested array for comparing changed options - $old = array_map('json_encode', get_object_vars($this->options)); - $new = array_map('json_encode', get_object_vars($settings)); + $old = get_object_vars($this->options); + $new = get_object_vars($settings); + $changed = array_udiff($old, $new, function($a, $b) { + // custom callback since array_diff uses strings for comparison - return array_keys(array_diff($old, $new)); + return $a <=> $b; + }); + + return array_keys($changed); } } From 1f90b4e3937f6bae37a74eade30ed73ce43faa7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 09:25:09 +0100 Subject: [PATCH 17/29] Update options one by one to update all instance --- src/Server/Workspace.php | 15 +++++++-------- .../Workspace/DidChangeConfigurationTest.php | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 7f76977..e74490e 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -225,18 +225,17 @@ class Workspace } $changedOptions = $this->getChangedOptions($settings); - $this->options = $settings; - if (!empty(array_intersect($changedOptions, $this->options->getIndexerOptions()))) { + foreach (get_object_vars($settings) as $prop => $val) { + $this->options->$prop = $val; + } + + if ($this->indexer && !empty(array_intersect($changedOptions, $this->options->getIndexerOptions()))) { // check list of options that changed since last time against the list of valid indexer options - // start wiping from the main index + // wipe main index and start reindexing $this->index->wipe(); - - // check for existing indexer and start indexing - if ($this->indexer) { - $this->indexer->index()->otherwise('\\LanguageServer\\crash'); - } + $this->indexer->index()->otherwise('\\LanguageServer\\crash'); } } diff --git a/tests/Server/Workspace/DidChangeConfigurationTest.php b/tests/Server/Workspace/DidChangeConfigurationTest.php index 18970e9..8479764 100644 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -51,7 +51,7 @@ class DidChangeConfigurationTest extends ServerTestCase if ($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 (strpos($msg->body->params->message, 'All 0 PHP files parsed') !== false) { + } elseif (strpos($msg->body->params->message, 'All 1 PHP files parsed') !== false) { $promise->fulfill(); } } From ca225ff6a6baf6228d22f6e9c6e94aa83f6a17f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 12:30:18 +0100 Subject: [PATCH 18/29] Remove emitting wipe events --- src/Index/AbstractAggregateIndex.php | 3 --- src/Index/Index.php | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 39c1a1d..33b86e5 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -42,9 +42,6 @@ abstract class AbstractAggregateIndex implements ReadableIndex $index->on('definition-added', function () { $this->emit('definition-added'); }); - $index->on('wipe', function() { - $this->emit('wipe'); - }); } /** diff --git a/src/Index/Index.php b/src/Index/Index.php index e912087..9879ced 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -218,7 +218,5 @@ class Index implements ReadableIndex, \Serializable $this->references = []; $this->complete = false; $this->staticComplete = false; - - $this->emit('wipe'); } } From c4568bfc34b34732af5651640c0659b00565c9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 12:32:10 +0100 Subject: [PATCH 19/29] Accept different types/formats from clients Currently only the default Options type and the vscode format are accepted. --- src/Server/Workspace.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index e74490e..7527e2e 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -216,16 +216,38 @@ class Workspace } /** - * @param Options|null $settings + * Fires when client changes settings in the client + * + * The default paramter type is Options but it also accepts different types + * which will be transformed on demand. + * + * Currently only the vscode format is supported + * + * @param mixed|null $settings + * @return void */ - public function didChangeConfiguration(Options $settings = null) + public function didChangeConfiguration($settings = null) { if ($settings === null) { return; } + // VSC sends the settings with the config section as main key + if ($settings instanceof \stdClass && $settings->phpIntelliSense) { + $mapper = new \JsonMapper(); + $settings = $mapper->map($settings->phpIntelliSense, new Options); + } + + if (!($settings instanceof Options)) { + return; + } + $changedOptions = $this->getChangedOptions($settings); + if (empty($changedOptions)) { + return; + } + foreach (get_object_vars($settings) as $prop => $val) { $this->options->$prop = $val; } From 5308e7a6bcba16b3a96d4b5245bdf53853a73dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 12:55:46 +0100 Subject: [PATCH 20/29] Add new tests and update old ones --- src/Server/Workspace.php | 13 +- tests/Server/ServerTestCase.php | 21 +-- .../Workspace/DidChangeConfigurationTest.php | 143 ++++++++++++++---- 3 files changed, 127 insertions(+), 50 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 7527e2e..98184ba 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -224,12 +224,13 @@ class Workspace * Currently only the vscode format is supported * * @param mixed|null $settings - * @return void + * @return bool + * @throws \Exception Settings format not valid */ - public function didChangeConfiguration($settings = null) + public function didChangeConfiguration($settings = null): bool { if ($settings === null) { - return; + return false; } // VSC sends the settings with the config section as main key @@ -239,13 +240,13 @@ class Workspace } if (!($settings instanceof Options)) { - return; + throw new \Exception('Settings format not valid.'); } $changedOptions = $this->getChangedOptions($settings); if (empty($changedOptions)) { - return; + return false; } foreach (get_object_vars($settings) as $prop => $val) { @@ -259,6 +260,8 @@ class Workspace $this->index->wipe(); $this->indexer->index()->otherwise('\\LanguageServer\\crash'); } + + return true; } /** diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 7bee051..5d9e309 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -31,10 +31,6 @@ abstract class ServerTestCase extends TestCase */ protected $documentLoader; - protected $projectIndex; - protected $input; - protected $output; - /** * Map from FQN to Location of definition * @@ -53,23 +49,20 @@ abstract class ServerTestCase extends TestCase { $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; - $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); - $this->projectIndex->setComplete(); + $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $projectIndex->setComplete(); $rootPath = realpath(__DIR__ . '/../../fixtures/'); $options = new Options; $filesFinder = new FileSystemFilesFinder; $cache = new FileSystemCache; - $this->input = new MockProtocolStream; - $this->output = new MockProtocolStream; - - $definitionResolver = new DefinitionResolver($this->projectIndex); - $client = new LanguageClient($this->input, $this->output); - $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $this->projectIndex, $definitionResolver); - $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $this->projectIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $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); $indexer = new Indexer($filesFinder, $rootPath, $client, $cache, $dependenciesIndex, $sourceIndex, $this->documentLoader, null, null, $options); - $this->workspace = new Server\Workspace($client, $this->projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader, null, $indexer, $options); + $this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader, null, $indexer, $options); $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 index 8479764..e0ad2ea 100644 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -3,51 +3,135 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server\Workspace; -use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; -use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, Options}; -use LanguageServer\Protocol\{ - Message, - MessageType, - TextDocumentItem, - TextDocumentIdentifier, - SymbolInformation, - SymbolKind, - DiagnosticSeverity, - FormattingOptions, - Location, - Range, - Position -}; -use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; -use function LanguageServer\pathToUri; +use LanguageServer\Tests\MockProtocolStream; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver, Options, Indexer}; +use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index}; +use LanguageServer\ContentRetriever\FileSystemContentRetriever; +use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities, Message, MessageType}; +use LanguageServer\FilesFinder\FileSystemFilesFinder; +use LanguageServer\Cache\FileSystemCache; +use LanguageServer\Server\Workspace; use Sabre\Event\Promise; use Exception; class DidChangeConfigurationTest extends ServerTestCase { - public function testWipingIndex() + /** + * didChangeConfiguration does not need to do anything when no options/settings are passed + */ + public function test_no_option_passed() { - $promise = new Promise; + $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); + + $result = $workspace->didChangeConfiguration(); + $this->assertFalse($result); + } + + /** + * When the passed options/settings do not differ from the previous, it has nothing to do + */ + public function test_fails_with_invalid_options_type_or_format() + { + $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, null, null, null, $options); + + $this->expectException(\Exception::class); + $this->workspace->didChangeConfiguration(['invalid' => 'options format']); + } + + /** + * When the passed options/settings do not differ from the previous, it has nothing to do + */ + public function test_no_changed_options() + { + $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, null, null, null, $options); + + $result = $this->workspace->didChangeConfiguration($options); + $this->assertFalse($result); + } + + /** + * Verify that the required methods for a reindex are called + */ + public function test_fileTypes_option_triggers_a_reindex() + { + $sourceIndex = new Index; + $dependenciesIndex = new DependenciesIndex; + $projectIndex = $this->getMockBuilder('LanguageServer\Index\ProjectIndex') + ->setConstructorArgs([$sourceIndex, $dependenciesIndex]) + ->setMethods(['wipe']) + ->getMock(); + $projectIndex->setComplete(); + + $rootPath = realpath(__DIR__ . '/../../../fixtures/'); + $filesFinder = new FileSystemFilesFinder; + $cache = new FileSystemCache; + + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); + $documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex); + $indexer = $this->getMockBuilder('LanguageServer\Indexer') + ->setConstructorArgs([$filesFinder, $rootPath, $client, $cache, $dependenciesIndex, $sourceIndex, $documentLoader, null, null, new Options]) + ->setMethods(['index']) + ->getMock(); + $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $documentLoader, null, $indexer, new Options); - $this->projectIndex->on('wipe', function() use ($promise) { - $promise->fulfill(); - }); $options = new Options; $options->fileTypes = [ '.inc' ]; - $this->workspace->didChangeConfiguration($options); - $promise->wait(); + $projectIndex->expects($this->once())->method('wipe'); + $indexer->expects($this->once())->method('index'); + + // invoke event + $result = $workspace->didChangeConfiguration($options); + $this->assertTrue($result); } - public function testReindexingAfterWipe() + /** + * Be sure that the indexer gets the new options/settings and uses them + */ + public function test_indexer_uses_new_options() { $promise = new Promise; + $sourceIndex = new Index; + $dependenciesIndex = new DependenciesIndex; + $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $projectIndex->setComplete(); - $this->output->on('message', function (Message $msg) use ($promise) { + $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, null, null, $initialOptions); + $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $documentLoader, null, $indexer, $initialOptions); + + $output->on('message', function (Message $msg) use ($promise) { if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); @@ -62,11 +146,8 @@ class DidChangeConfigurationTest extends ServerTestCase '.inc' ]; - $this->workspace->didChangeConfiguration($options); + $result = $workspace->didChangeConfiguration($options); + $this->assertTrue($result); $promise->wait(); } - - public function testGetChangedOptions() - { - } } From a06057b7a33d9f42ad956265b50562cbeaacb7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 12:59:22 +0100 Subject: [PATCH 21/29] Fix phpcs warnings/errors --- src/Server/Workspace.php | 2 +- tests/Server/Workspace/DidChangeConfigurationTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 98184ba..ad724de 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -274,7 +274,7 @@ class Workspace { $old = get_object_vars($this->options); $new = get_object_vars($settings); - $changed = array_udiff($old, $new, function($a, $b) { + $changed = array_udiff($old, $new, function ($a, $b) { // custom callback since array_diff uses strings for comparison return $a <=> $b; diff --git a/tests/Server/Workspace/DidChangeConfigurationTest.php b/tests/Server/Workspace/DidChangeConfigurationTest.php index e0ad2ea..6028c6c 100644 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -20,7 +20,7 @@ class DidChangeConfigurationTest extends ServerTestCase /** * didChangeConfiguration does not need to do anything when no options/settings are passed */ - public function test_no_option_passed() + public function testNoOptionPassed() { $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); $projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex()); @@ -35,7 +35,7 @@ class DidChangeConfigurationTest extends ServerTestCase /** * When the passed options/settings do not differ from the previous, it has nothing to do */ - public function test_fails_with_invalid_options_type_or_format() + public function testFailsWithInvalidOptionsTypeOrFormat() { $options = new Options; $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); @@ -51,7 +51,7 @@ class DidChangeConfigurationTest extends ServerTestCase /** * When the passed options/settings do not differ from the previous, it has nothing to do */ - public function test_no_changed_options() + public function testNoChangedOptions() { $options = new Options; $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); @@ -67,7 +67,7 @@ class DidChangeConfigurationTest extends ServerTestCase /** * Verify that the required methods for a reindex are called */ - public function test_fileTypes_option_triggers_a_reindex() + public function testFileTypesOptionTriggersAReindex() { $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; @@ -108,7 +108,7 @@ class DidChangeConfigurationTest extends ServerTestCase /** * Be sure that the indexer gets the new options/settings and uses them */ - public function test_indexer_uses_new_options() + public function testIndexerUsesNewOptions() { $promise = new Promise; $sourceIndex = new Index; From 23a40f069b7ab0aafc73bd67b7059f541d51e091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 13:02:16 +0100 Subject: [PATCH 22/29] Let didChangeConfiguration decide what options are interesting for the indexer --- src/Options.php | 15 --------------- src/Server/Workspace.php | 5 ++++- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Options.php b/src/Options.php index 9f61edb..99f1aab 100644 --- a/src/Options.php +++ b/src/Options.php @@ -11,11 +11,6 @@ class Options */ public $fileTypes = ['.php']; - /** - * List of options that affect the indexer - */ - private $indexerOptions = ['fileTypes']; - /** * Validate/Filter input and set options for file types * @@ -31,16 +26,6 @@ class Options $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; } - /** - * Get list with options that affect the indexer - * - * @return array - */ - public function getIndexerOptions(): array - { - return $this->indexerOptions; - } - /** * Filter valid file type * diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index ad724de..b983bf6 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -229,6 +229,9 @@ class Workspace */ public function didChangeConfiguration($settings = null): bool { + // List of options that affect the indexer + $indexerOptions = ['fileTypes']; + if ($settings === null) { return false; } @@ -253,7 +256,7 @@ class Workspace $this->options->$prop = $val; } - if ($this->indexer && !empty(array_intersect($changedOptions, $this->options->getIndexerOptions()))) { + if ($this->indexer && !empty(array_intersect($changedOptions, $indexerOptions))) { // check list of options that changed since last time against the list of valid indexer options // wipe main index and start reindexing From f4f106766f8fcf4bab1ce6be6a06888e232528ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 4 Mar 2017 17:26:23 +0100 Subject: [PATCH 23/29] Change didChangeConfiguration doc to protocol wording --- src/Server/Workspace.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index b983bf6..954e5e9 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -216,7 +216,7 @@ class Workspace } /** - * Fires when client changes settings in the client + * A notification sent from the client to the server to signal the change of configuration settings. * * The default paramter type is Options but it also accepts different types * which will be transformed on demand. From 09fbec247c22f2e5355652c47f55c6d2d54fd2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Wed, 29 Aug 2018 21:34:50 +0200 Subject: [PATCH 24/29] Refactor pull request * merge latest upstream * remove currently not required code blocks * fix tests --- src/Index/AbstractAggregateIndex.php | 10 -- src/Index/Index.php | 8 - src/Indexer.php | 34 ++-- src/LanguageServer.php | 1 + src/Options.php | 11 +- tests/LanguageServerTest.php | 16 +- .../Workspace/DidChangeConfigurationTest.php | 153 ------------------ 7 files changed, 31 insertions(+), 202 deletions(-) delete mode 100644 tests/Server/Workspace/DidChangeConfigurationTest.php diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 33b86e5..5377c3a 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -147,14 +147,4 @@ 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 a88c7ba..9cb975e 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -222,12 +222,4 @@ class Index implements ReadableIndex, \Serializable 'staticComplete' => $this->staticComplete ]); } - - public function wipe() - { - $this->definitions = []; - $this->references = []; - $this->complete = false; - $this->staticComplete = false; - } } diff --git a/src/Indexer.php b/src/Indexer.php index fd59aa3..e411054 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -53,6 +53,11 @@ class Indexer */ private $documentLoader; + /** + * @var Options + */ + private $options; + /** * @var \stdClasss */ @@ -64,20 +69,15 @@ class Indexer private $composerJson; /** - * @var Options - */ - private $options; - - /** - * @param FilesFinder $filesFinder - * @param string $rootPath - * @param LanguageClient $client - * @param Cache $cache - * @param DependenciesIndex $dependenciesIndex - * @param Index $sourceIndex - * @param PhpDocumentLoader $documentLoader - * @param \stdClass|null $composerLock - * @param Options|null $options + * @param FilesFinder $filesFinder + * @param string $rootPath + * @param LanguageClient $client + * @param Cache $cache + * @param DependenciesIndex $dependenciesIndex + * @param Index $sourceIndex + * @param Options $options + * @param PhpDocumentLoader $documentLoader + * @param \stdClass|null $composerLock */ public function __construct( FilesFinder $filesFinder, @@ -87,9 +87,9 @@ class Indexer DependenciesIndex $dependenciesIndex, Index $sourceIndex, PhpDocumentLoader $documentLoader, + Options $options, \stdClass $composerLock = null, - \stdClass $composerJson = null, - Options $options = null + \stdClass $composerJson = null ) { $this->filesFinder = $filesFinder; $this->rootPath = $rootPath; @@ -98,9 +98,9 @@ class Indexer $this->dependenciesIndex = $dependenciesIndex; $this->sourceIndex = $sourceIndex; $this->documentLoader = $documentLoader; + $this->options = $options; $this->composerLock = $composerLock; $this->composerJson = $composerJson; - $this->options = $options; } /** diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 193b0a1..b29cb1d 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -232,6 +232,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $dependenciesIndex, $sourceIndex, $this->documentLoader, + $initializationOptions, $this->composerLock, $this->composerJson, $initializationOptions diff --git a/src/Options.php b/src/Options.php index 99f1aab..48ba540 100644 --- a/src/Options.php +++ b/src/Options.php @@ -1,11 +1,12 @@ [$this, 'filterFileTypes']]); // validate file type format - $fileTypes = array_filter($fileTypes, 'strlen'); // filter empty items - $fileTypes = array_values($fileTypes); //rebase indexes + $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; } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 3354b68..5896eac 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -30,7 +30,7 @@ class LanguageServerTest extends TestCase public function testInitialize() { $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream); - $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait(); + $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid(), new Options)->wait(); $serverCapabilities = new ServerCapabilities(); $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; @@ -67,7 +67,7 @@ class LanguageServerTest extends TestCase }); $server = new LanguageServer($input, $output); $capabilities = new ClientCapabilities; - $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), new Options); $promise->wait(); } @@ -115,7 +115,7 @@ class LanguageServerTest extends TestCase $capabilities = new ClientCapabilities; $capabilities->xfilesProvider = true; $capabilities->xcontentProvider = true; - $server->initialize($capabilities, $rootPath, getmypid()); + $server->initialize($capabilities, $rootPath, getmypid(), new Options); $promise->wait(); $this->assertTrue($filesCalled); $this->assertTrue($contentCalled); @@ -127,24 +127,22 @@ class LanguageServerTest extends TestCase $input = new MockProtocolStream; $output = new MockProtocolStream; $options = new Options; - $options->setFileTypes([ '.php', '.inc' ]); - - $output->on('message', function (Message $msg) use ($promise, &$foundFiles) { + $output->on('message', function (Message $msg) use ($promise, &$allFilesParsed) { if ($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 (strpos($msg->body->params->message, 'All 27 PHP files parsed') !== false) { - $promise->fulfill(); + } 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(), $options); - $promise->wait(); + $this->assertTrue($promise->wait()); } } diff --git a/tests/Server/Workspace/DidChangeConfigurationTest.php b/tests/Server/Workspace/DidChangeConfigurationTest.php deleted file mode 100644 index 6028c6c..0000000 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ /dev/null @@ -1,153 +0,0 @@ -didChangeConfiguration(); - $this->assertFalse($result); - } - - /** - * When the passed options/settings do not differ from the previous, it has nothing to do - */ - public function testFailsWithInvalidOptionsTypeOrFormat() - { - $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, null, null, null, $options); - - $this->expectException(\Exception::class); - $this->workspace->didChangeConfiguration(['invalid' => 'options format']); - } - - /** - * When the passed options/settings do not differ from the previous, it has nothing to do - */ - public function testNoChangedOptions() - { - $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, null, null, null, $options); - - $result = $this->workspace->didChangeConfiguration($options); - $this->assertFalse($result); - } - - /** - * Verify that the required methods for a reindex are called - */ - public function testFileTypesOptionTriggersAReindex() - { - $sourceIndex = new Index; - $dependenciesIndex = new DependenciesIndex; - $projectIndex = $this->getMockBuilder('LanguageServer\Index\ProjectIndex') - ->setConstructorArgs([$sourceIndex, $dependenciesIndex]) - ->setMethods(['wipe']) - ->getMock(); - $projectIndex->setComplete(); - - $rootPath = realpath(__DIR__ . '/../../../fixtures/'); - $filesFinder = new FileSystemFilesFinder; - $cache = new FileSystemCache; - - $definitionResolver = new DefinitionResolver($projectIndex); - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); - $textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex); - $indexer = $this->getMockBuilder('LanguageServer\Indexer') - ->setConstructorArgs([$filesFinder, $rootPath, $client, $cache, $dependenciesIndex, $sourceIndex, $documentLoader, null, null, new Options]) - ->setMethods(['index']) - ->getMock(); - $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $documentLoader, null, $indexer, new Options); - - - $options = new Options; - $options->fileTypes = [ - '.inc' - ]; - - $projectIndex->expects($this->once())->method('wipe'); - $indexer->expects($this->once())->method('index'); - - // invoke event - $result = $workspace->didChangeConfiguration($options); - $this->assertTrue($result); - } - - /** - * Be sure that the indexer gets the new options/settings and uses them - */ - public function testIndexerUsesNewOptions() - { - $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, null, null, $initialOptions); - $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $documentLoader, null, $indexer, $initialOptions); - - $output->on('message', function (Message $msg) use ($promise) { - if ($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 (strpos($msg->body->params->message, 'All 1 PHP files parsed') !== false) { - $promise->fulfill(); - } - } - }); - - $options = new Options; - $options->fileTypes = [ - '.inc' - ]; - - $result = $workspace->didChangeConfiguration($options); - $this->assertTrue($result); - $promise->wait(); - } -} From a5417cdf72256ae8a928dcabe2169a3bd01d7f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Wed, 29 Aug 2018 21:35:18 +0200 Subject: [PATCH 25/29] Fix risky test warning --- tests/LanguageServerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 5896eac..9fe890d 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -61,14 +61,14 @@ class LanguageServerTest extends TestCase 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(), new Options); - $promise->wait(); + $this->assertTrue($promise->wait()); } public function testIndexingWithFilesAndContentRequests() From e317e8c743a4b71cf5a97dca23c1cf67901f6685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Fri, 31 Aug 2018 11:54:52 +0200 Subject: [PATCH 26/29] Start indexing after initialization The indexer is moved to the method initialized, so we can request configurations from the client to init the indexer itself. --- src/Client/Workspace.php | 21 ++++ src/Index/ProjectIndex.php | 2 +- src/LanguageServer.php | 160 ++++++++++++++++++----------- src/Protocol/ConfigurationItem.php | 20 ++++ tests/LanguageServerTest.php | 42 +++++--- 5 files changed, 171 insertions(+), 74 deletions(-) create mode 100644 src/Protocol/ConfigurationItem.php diff --git a/src/Client/Workspace.php b/src/Client/Workspace.php index 901e386..2057340 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Client; use LanguageServer\ClientHandler; +use LanguageServer\Protocol\ConfigurationItem; use LanguageServer\Protocol\TextDocumentIdentifier; use Sabre\Event\Promise; use JsonMapper; @@ -44,4 +45,24 @@ class Workspace 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 + */ + public function configuration(array $items): Promise + { + return $this->handler->request( + 'workspace/configuration', + ['items' => $items] + ); + } } 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/LanguageServer.php b/src/LanguageServer.php index b29cb1d..00822d7 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,16 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ protected $definitionResolver; + /** + * @var string|null + */ + protected $rootPath; + + /** + * @var Cache + */ + protected $cache; + /** * @param ProtocolReader $reader * @param ProtocolWriter $writer @@ -162,14 +173,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 Options $initializationOptions The options send from client to initialize the server + * @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, Options $initializationOptions = null): Promise - { - return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) { - + 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 { @@ -187,82 +202,48 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); - $initializationOptions = $initializationOptions ?? new Options; + $this->rootPath = $rootPath; // 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; - - // Index in background - $indexer = new Indexer( - $this->filesFinder, - $rootPath, - $this->client, - $cache, - $dependenciesIndex, - $sourceIndex, - $this->documentLoader, - $initializationOptions, - $this->composerLock, - $this->composerJson, - $initializationOptions - ); - $indexer->index()->otherwise('\\LanguageServer\\crash'); - } - - - if ($this->textDocument === null) { - $this->textDocument = new Server\TextDocument( - $this->documentLoader, - $this->definitionResolver, - $this->client, - $this->globalIndex, - $this->composerJson, - $this->composerLock - ); - } - if ($this->workspace === null) { - $this->workspace = new Server\Workspace( - $this->client, - $this->projectIndex, - $dependenciesIndex, - $sourceIndex, - $this->composerLock, - $this->documentLoader, - $this->composerJson, - $indexer, - $initializationOptions - ); + $this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache; } $serverCapabilities = new ServerCapabilities(); @@ -295,10 +276,71 @@ 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 () { + list($sourceIndex, $dependenciesIndex) = $this->projectIndex->getIndexes(); + $mapper = new \JsonMapper(); + $configurationitem = new ConfigurationItem(); + $configurationitem->section = 'php'; + $configuration = yield $this->client->workspace->configuration([$configurationitem]); + $options = $mapper->map($configuration[0], new Options()); + + if ($this->rootPath) { + // Index in background + $indexer = new Indexer( + $this->filesFinder, + $this->rootPath, + $this->client, + $this->cache, + $dependenciesIndex, + $sourceIndex, + $this->documentLoader, + $options, + $this->composerLock, + $this->composerJson + ); + + $indexer->index()->otherwise('\\LanguageServer\\crash'); + } + + if ($this->textDocument === null) { + $this->textDocument = new Server\TextDocument( + $this->documentLoader, + $this->definitionResolver, + $this->client, + $this->globalIndex, + $this->composerJson, + $this->composerLock + ); + } + + if ($this->workspace === null) { + $this->workspace = new Server\Workspace( + $this->client, + $this->projectIndex, + $dependenciesIndex, + $sourceIndex, + $options, + $this->composerLock, + $this->documentLoader, + $this->composerJson + ); + } + }); + } + /** * 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/Protocol/ConfigurationItem.php b/src/Protocol/ConfigurationItem.php new file mode 100644 index 0000000..5ef9a27 --- /dev/null +++ b/src/Protocol/ConfigurationItem.php @@ -0,0 +1,20 @@ +initialize(new ClientCapabilities, __DIR__, getmypid(), new Options)->wait(); + $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait(); $serverCapabilities = new ServerCapabilities(); $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; @@ -56,8 +56,13 @@ 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)) { @@ -67,7 +72,8 @@ class LanguageServerTest extends TestCase }); $server = new LanguageServer($input, $output); $capabilities = new ClientCapabilities; - $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), new Options); + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); + $server->initialized(); $this->assertTrue($promise->wait()); } @@ -81,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; @@ -115,7 +126,8 @@ class LanguageServerTest extends TestCase $capabilities = new ClientCapabilities; $capabilities->xfilesProvider = true; $capabilities->xcontentProvider = true; - $server->initialize($capabilities, $rootPath, getmypid(), new Options); + $server->initialize($capabilities, $rootPath, getmypid())->wait(); + $server->initialized(); $promise->wait(); $this->assertTrue($filesCalled); $this->assertTrue($contentCalled); @@ -126,13 +138,14 @@ class LanguageServerTest extends TestCase $promise = new Promise; $input = new MockProtocolStream; $output = new MockProtocolStream; - $options = new Options; - $options->setFileTypes([ - '.php', - '.inc' - ]); - $output->on('message', function (Message $msg) use ($promise, &$allFilesParsed) { - 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', '.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)) { @@ -142,7 +155,8 @@ class LanguageServerTest extends TestCase }); $server = new LanguageServer($input, $output); $capabilities = new ClientCapabilities; - $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), $options); + $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid()); + $server->initialized(); $this->assertTrue($promise->wait()); } } From a1c3845c9f06f9390675702e37f4de83dfb5b1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Fri, 31 Aug 2018 14:40:23 +0200 Subject: [PATCH 27/29] WIP: Implement didChangeConfiguration with reindexing --- src/Index/AbstractAggregateIndex.php | 10 + src/Index/Index.php | 11 + src/Indexer.php | 46 ++++ src/LanguageServer.php | 1 + src/Server/Workspace.php | 82 ++++++- tests/Server/ServerTestCase.php | 7 +- .../Workspace/DidChangeConfigurationTest.php | 231 ++++++++++++++++++ .../Workspace/DidChangeWatchedFilesTest.php | 5 +- 8 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 tests/Server/Workspace/DidChangeConfigurationTest.php 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); From a1e56543c3e292c68d60d9a43d4b91ac5970da95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Fri, 31 Aug 2018 20:49:23 +0200 Subject: [PATCH 28/29] WIP: Implement didChangeConfiguration with reindexing * Handle the case where didChangeConfiguration is called before workspace/configuration request is resolved. * Implement basic cancellation signal request * Use defaults options and only apply new on request --- src/Indexer.php | 20 +++- src/LanguageServer.php | 112 +++++++++++-------- src/Protocol/ClientCapabilities.php | 5 + src/Protocol/WorkspaceClientCapabilities.php | 11 ++ src/Server/Workspace.php | 71 +++++++----- 5 files changed, 142 insertions(+), 77 deletions(-) create mode 100644 src/Protocol/WorkspaceClientCapabilities.php diff --git a/src/Indexer.php b/src/Indexer.php index 253c7d7..93812b9 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -85,7 +85,6 @@ class Indexer * @param Cache $cache * @param DependenciesIndex $dependenciesIndex * @param Index $sourceIndex - * @param Options $options * @param PhpDocumentLoader $documentLoader * @param \stdClass|null $composerLock */ @@ -97,7 +96,6 @@ class Indexer DependenciesIndex $dependenciesIndex, Index $sourceIndex, PhpDocumentLoader $documentLoader, - Options $options, \stdClass $composerLock = null, \stdClass $composerJson = null ) { @@ -108,11 +106,24 @@ class Indexer $this->dependenciesIndex = $dependenciesIndex; $this->sourceIndex = $sourceIndex; $this->documentLoader = $documentLoader; - $this->options = $options; $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; } /** @@ -156,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); @@ -243,6 +255,7 @@ class Indexer } $this->hasCancellationSignal = false; + $this->client->window->logMessage(MessageType::INFO, 'Indexing project canceled'); }); } @@ -254,6 +267,7 @@ class Indexer { return coroutine(function () use ($files) { foreach ($files as $i => $uri) { + // abort current running indexing if ($this->hasCancellationSignal) { return; } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 29cfc4c..4a04d73 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -117,6 +117,16 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ protected $cache; + /** + * @var ClientCapabilities + */ + protected $clientCapabilities; + + /** + * @var Indexer + */ + protected $indexer; + /** * @param ProtocolReader $reader * @param ProtocolWriter $writer @@ -203,6 +213,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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); @@ -244,6 +255,43 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } $this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache; + + // Index in background + $this->indexer = new Indexer( + $this->filesFinder, + $this->rootPath, + $this->client, + $this->cache, + $dependenciesIndex, + $sourceIndex, + $this->documentLoader, + $this->composerLock, + $this->composerJson + ); + } + + if ($this->textDocument === null) { + $this->textDocument = new Server\TextDocument( + $this->documentLoader, + $this->definitionResolver, + $this->client, + $this->globalIndex, + $this->composerJson, + $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 + ); } $serverCapabilities = new ServerCapabilities(); @@ -286,55 +334,31 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher public function initialized(): Promise { return coroutine(function () { - list($sourceIndex, $dependenciesIndex) = $this->projectIndex->getIndexes(); - $mapper = new \JsonMapper(); - $configurationitem = new ConfigurationItem(); - $configurationitem->section = 'php'; - $configuration = yield $this->client->workspace->configuration([$configurationitem]); - $options = $mapper->map($configuration[0], new Options()); - - if ($this->rootPath) { - // Index in background - $indexer = new Indexer( - $this->filesFinder, - $this->rootPath, - $this->client, - $this->cache, - $dependenciesIndex, - $sourceIndex, - $this->documentLoader, - $options, - $this->composerLock, - $this->composerJson - ); - - $indexer->index()->otherwise('\\LanguageServer\\crash'); + if (!$this->rootPath) { + return; } - if ($this->textDocument === null) { - $this->textDocument = new Server\TextDocument( - $this->documentLoader, - $this->definitionResolver, - $this->client, - $this->globalIndex, - $this->composerJson, - $this->composerLock - ); + // request configuration if it is supported + // support comes with protocol version 3.6.0 + if ($this->clientCapabilities->workspace->configuration) { + $configurationitem = new ConfigurationItem(); + $configurationitem->section = 'php'; + $configuration = yield $this->client->workspace->configuration([$configurationitem]); + $options = $this->mapper->map($configuration[0], new Options()); } - if ($this->workspace === null) { - $this->workspace = new Server\Workspace( - $this->client, - $this->projectIndex, - $dependenciesIndex, - $sourceIndex, - $options, - $indexer, - $this->composerLock, - $this->documentLoader, - $this->composerJson - ); + // 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'); }); } 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/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 @@ +composerLock = $composerLock; $this->documentLoader = $documentLoader; $this->composerJson = $composerJson; - $this->options = $options; $this->indexer = $indexer; } @@ -205,36 +197,32 @@ class Workspace /** * 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 + * @param mixed $settings Settings as JSON object structure with php as primary key * @return Promise */ - public function didChangeConfiguration(\stdClass $settings): Promise + public function didChangeConfiguration($settings): Promise { - xdebug_break(); return coroutine(function () use ($settings) { + if (!property_exists($settings, 'php') || $settings->php === new \stdClass()) { + return; + } + try { - xdebug_break(); $mapper = new \JsonMapper(); - $settings = $mapper->map($settings->php, new Options); + $options = $mapper->map($settings->php, new Options); - if ($this->options == $settings) { - return; + // 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(); + $this->indexer->index()->otherwise('\\LanguageServer\\crash'); } - - // @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, @@ -244,4 +232,27 @@ class Workspace } }); } + + /** + * 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; + } } From a81bed93c75f0e33ccfcb32c355594a046240ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Steitz?= Date: Sat, 1 Sep 2018 00:10:36 +0200 Subject: [PATCH 29/29] Partial work on feedback --- src/Client/Workspace.php | 2 +- src/LanguageServer.php | 4 +--- src/Protocol/ConfigurationItem.php | 7 +++++++ src/Server/Workspace.php | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Client/Workspace.php b/src/Client/Workspace.php index 2057340..820fc45 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -56,7 +56,7 @@ class Workspace * the result for the first configuration item in the params). * * @param ConfigurationItem[] $items - * @return Promise + * @return Promise */ public function configuration(array $items): Promise { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 4a04d73..9548b7d 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -341,9 +341,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // request configuration if it is supported // support comes with protocol version 3.6.0 if ($this->clientCapabilities->workspace->configuration) { - $configurationitem = new ConfigurationItem(); - $configurationitem->section = 'php'; - $configuration = yield $this->client->workspace->configuration([$configurationitem]); + $configuration = yield $this->client->workspace->configuration([new ConfigurationItem('php')]); $options = $this->mapper->map($configuration[0], new Options()); } diff --git a/src/Protocol/ConfigurationItem.php b/src/Protocol/ConfigurationItem.php index 5ef9a27..dff8b95 100644 --- a/src/Protocol/ConfigurationItem.php +++ b/src/Protocol/ConfigurationItem.php @@ -1,4 +1,5 @@ section = $section; + $this->scopeUri = $scopeUri; + } } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index ac8f92b..61230e6 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -221,7 +221,7 @@ class Workspace } $this->projectIndex->wipe(); - $this->indexer->index()->otherwise('\\LanguageServer\\crash'); + yield $this->indexer->index(); } } catch (\JsonMapper_Exception $exception) { $this->client->window->showMessage(