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 34ad618f..3529e19f 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 @@ public function __construct( 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 @@ public function __construct( $this->documentLoader = $documentLoader; $this->composerLock = $composerLock; $this->composerJson = $composerJson; + $this->options = $options; } /** @@ -104,8 +112,8 @@ public function __construct( public function index(): Promise { return coroutine(function () { - - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); + $fileTypes = implode(',', $this->options->fileTypes); + $pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath); $uris = yield $this->filesFinder->find($pattern); $count = count($uris); diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 3c999f22..12ee1736 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -167,11 +167,12 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. + * @param 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 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $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 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $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 00000000..3a4842eb --- /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 00000000..b3d9bbc7 --- /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 3529e19f..4cd85d36 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 4cd85d36..c341dd29 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -112,7 +112,7 @@ public function __construct( 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 3a4842eb..d7e78847 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); + } + } + + /** + * Validate 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); + + $this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes; + } + + /** + * Get list of registered file types + * + * @return array + */ + public function getFileTypes(): array + { + return $this->fileTypes; } - private function normalizeFileTypes(array $fileTypes): array + /** + * 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) { - return array_map(function (string $fileType) { - if (substr($fileType, 0, 1) !== '.') { - $fileType = '.' . $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 c341dd29..4cd85d36 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -112,7 +112,7 @@ public function __construct( 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 12ee1736..7e5074b6 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -167,10 +167,10 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. - * @param 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 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); - $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 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->documentLoader, $this->composerLock, $this->composerJson, - $options + $initializationOptions ); $indexer->index()->otherwise('\\LanguageServer\\crash'); } diff --git a/src/Options.php b/src/Options.php index d7e78847..99f1aab9 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 00000000..d47afcf9 --- /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 7e5074b6..d3b1c5c8 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -191,6 +191,7 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); + $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 0c0740c3..9b169021 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; +use LanguageServer\Options; use LanguageServer\Protocol\{ Message, ClientCapabilities, @@ -123,12 +124,12 @@ public function testIndexingMultipleFileTypes() $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 d47afcf9..f2251d8b 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 f2251d8b..45e35f6d 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -6,7 +6,7 @@ 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 5377c3a4..39c1a1da 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -42,6 +42,9 @@ protected function registerIndex(ReadableIndex $index) $index->on('definition-added', function () { $this->emit('definition-added'); }); + $index->on('wipe', function() { + $this->emit('wipe'); + }); } /** @@ -147,4 +150,14 @@ public function getReferenceUris(string $fqn): array } return $refs; } + + /** + * Wipe all indexes for a reindex + */ + public function wipe() + { + foreach ($this->getIndexes() as $index) { + $index->wipe(); + } + } } diff --git a/src/Index/Index.php b/src/Index/Index.php index 5c24813e..e912087b 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -211,4 +211,14 @@ public function serialize() '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 b94618cb..3a10b78c 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -170,4 +170,9 @@ public function xdependencies(): array } 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 99f1aab9..9f61edb8 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 @@ public function setFileTypes(array $fileTypes) $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 3a10b78c..fb3807d1 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -3,7 +3,7 @@ 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 @@ public function __construct(ProjectIndex $index, DependenciesIndex $dependencies $this->composerLock = $composerLock; $this->documentLoader = $documentLoader; $this->composerJson = $composerJson; + $this->indexer = $indexer; + $this->options = $options; } /** @@ -171,8 +185,43 @@ public function xdependencies(): array return $dependencyReferences; } - public function didChangeConfiguration($settings = null) + /** + * @param Options|null $settings + */ + public function didChangeConfiguration(Options $settings = null) + { + 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 { - $this->index->wipe(); + // 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 d3b1c5c8..62b84007 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -261,7 +261,9 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $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 | 32 ++++++--- .../Workspace/DidChangeConfigurationTest.php | 72 +++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 tests/Server/Workspace/DidChangeConfigurationTest.php diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 679191f9..1a45c642 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,10 +5,12 @@ 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 @@ public function setUp() { $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; - $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); - $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); + $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $this->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); + $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 00000000..18970e93 --- /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 1f351342..7f769772 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -248,10 +248,14 @@ public function didChangeConfiguration(Options $settings = null) */ 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 7f769772..e74490e4 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -225,18 +225,17 @@ public function didChangeConfiguration(Options $settings = null) } $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 18970e93..8479764e 100644 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -51,7 +51,7 @@ public function testReindexingAfterWipe() 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 39c1a1da..33b86e55 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -42,9 +42,6 @@ protected function registerIndex(ReadableIndex $index) $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 e912087b..9879ced6 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -218,7 +218,5 @@ public function wipe() $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 e74490e4..7527e2e3 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -216,16 +216,38 @@ public function xdependencies(): array } /** - * @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 7527e2e3..98184ba6 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -224,12 +224,13 @@ public function xdependencies(): array * 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 @@ public function didChangeConfiguration($settings = null) } 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 @@ public function didChangeConfiguration($settings = null) $this->index->wipe(); $this->indexer->index()->otherwise('\\LanguageServer\\crash'); } + + return true; } /** diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 7bee0518..5d9e309f 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 @@ public function setUp() { $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 8479764e..e0ad2eae 100644 --- a/tests/Server/Workspace/DidChangeConfigurationTest.php +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -3,51 +3,135 @@ 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(); + + $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); - $this->output->on('message', function (Message $msg) use ($promise) { + $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 @@ public function testReindexingAfterWipe() '.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 98184ba6..ad724de1 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -274,7 +274,7 @@ private function getChangedOptions(Options $settings): array { $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 e0ad2eae..6028c6c8 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 @@ public function test_no_option_passed() /** * 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 @@ public function test_fails_with_invalid_options_type_or_format() /** * 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 @@ public function test_no_changed_options() /** * 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 @@ public function test_fileTypes_option_triggers_a_reindex() /** * 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 9f61edb8..99f1aab9 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 @@ public function setFileTypes(array $fileTypes) $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 ad724de1..b983bf6d 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -229,6 +229,9 @@ public function xdependencies(): array */ public function didChangeConfiguration($settings = null): bool { + // List of options that affect the indexer + $indexerOptions = ['fileTypes']; + if ($settings === null) { return false; } @@ -253,7 +256,7 @@ public function didChangeConfiguration($settings = null): bool $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 b983bf6d..954e5e9a 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -216,7 +216,7 @@ public function xdependencies(): array } /** - * 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 33b86e55..5377c3a4 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -147,14 +147,4 @@ public function getReferenceUris(string $fqn): array } return $refs; } - - /** - * Wipe all indexes for a reindex - */ - public function wipe() - { - foreach ($this->getIndexes() as $index) { - $index->wipe(); - } - } } diff --git a/src/Index/Index.php b/src/Index/Index.php index a88c7ba5..9cb975e5 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -222,12 +222,4 @@ public function serialize() '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 fd59aa39..e4110543 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -54,30 +54,30 @@ class Indexer private $documentLoader; /** - * @var \stdClasss + * @var Options */ - private $composerLock; + private $options; /** * @var \stdClasss */ - private $composerJson; + private $composerLock; /** - * @var Options + * @var \stdClasss */ - private $options; + 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 - * @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 @@ public function __construct( 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 @@ public function __construct( $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 193b0a11..b29cb1da 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -232,6 +232,7 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $dependenciesIndex, $sourceIndex, $this->documentLoader, + $initializationOptions, $this->composerLock, $this->composerJson, $initializationOptions diff --git a/src/Options.php b/src/Options.php index 99f1aab9..48ba540b 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 3354b68e..5896eaca 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 @@ public function testIndexingWithDirectFileAccess() }); $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 @@ public function testIndexingWithFilesAndContentRequests() $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 @@ public function testIndexingMultipleFileTypes() $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 6028c6c8..00000000 --- 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 5896eaca..9fe890d4 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -61,14 +61,14 @@ public function testIndexingWithDirectFileAccess() 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 | 152 ++++++++++++++++++----------- src/Protocol/ConfigurationItem.php | 20 ++++ tests/LanguageServerTest.php | 42 +++++--- 5 files changed, 167 insertions(+), 70 deletions(-) create mode 100644 src/Protocol/ConfigurationItem.php diff --git a/src/Client/Workspace.php b/src/Client/Workspace.php index 901e386a..20573402 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -4,6 +4,7 @@ namespace LanguageServer\Client; use LanguageServer\ClientHandler; +use LanguageServer\Protocol\ConfigurationItem; use LanguageServer\Protocol\TextDocumentIdentifier; use Sabre\Event\Promise; use JsonMapper; @@ -44,4 +45,24 @@ public function xfiles(string $base = null): Promise 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 af980f8f..bc03baf0 100644 --- a/src/Index/ProjectIndex.php +++ b/src/Index/ProjectIndex.php @@ -35,7 +35,7 @@ public function __construct(Index $sourceIndex, DependenciesIndex $dependenciesI /** * @return ReadableIndex[] */ - protected function getIndexes(): array + public function getIndexes(): array { return [$this->sourceIndex, $this->dependenciesIndex]; } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index b29cb1da..00822d77 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -4,6 +4,7 @@ namespace LanguageServer; use LanguageServer\Protocol\{ + ConfigurationItem, ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, @@ -15,7 +16,7 @@ use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; -use LanguageServer\Cache\{FileSystemCache, ClientCache}; +use LanguageServer\Cache\{Cache, FileSystemCache, ClientCache}; use AdvancedJsonRpc; use Sabre\Event\Promise; use function Sabre\Event\coroutine; @@ -106,6 +107,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 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer) * * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. - * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. - * @param 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,60 +202,115 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $stubsIndex = StubsIndex::read(); $this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); - $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; + $this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache; + } + + $serverCapabilities = new ServerCapabilities(); + // Ask the client to return always full documents (because we need to rebuild the AST from scratch) + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; + // Support "Find all symbols" + $serverCapabilities->documentSymbolProvider = true; + // Support "Find all symbols in workspace" + $serverCapabilities->workspaceSymbolProvider = true; + // Support "Go to definition" + $serverCapabilities->definitionProvider = true; + // Support "Find all references" + $serverCapabilities->referencesProvider = true; + // Support "Hover" + $serverCapabilities->hoverProvider = true; + // Support "Completion" + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(); + $serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ',']; + + // Support global references + $serverCapabilities->xworkspaceReferencesProvider = true; + $serverCapabilities->xdefinitionProvider = true; + $serverCapabilities->xdependenciesProvider = true; + return new InitializeResult($serverCapabilities); + }); + } + + /** + * 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, - $rootPath, + $this->rootPath, $this->client, - $cache, + $this->cache, $dependenciesIndex, $sourceIndex, $this->documentLoader, - $initializationOptions, + $options, $this->composerLock, - $this->composerJson, - $initializationOptions + $this->composerJson ); + $indexer->index()->otherwise('\\LanguageServer\\crash'); } - if ($this->textDocument === null) { $this->textDocument = new Server\TextDocument( $this->documentLoader, @@ -251,54 +321,26 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath = $this->composerLock ); } + if ($this->workspace === null) { $this->workspace = new Server\Workspace( $this->client, $this->projectIndex, $dependenciesIndex, $sourceIndex, + $options, $this->composerLock, $this->documentLoader, - $this->composerJson, - $indexer, - $initializationOptions + $this->composerJson ); } - - $serverCapabilities = new ServerCapabilities(); - // Ask the client to return always full documents (because we need to rebuild the AST from scratch) - $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; - // Support "Find all symbols" - $serverCapabilities->documentSymbolProvider = true; - // Support "Find all symbols in workspace" - $serverCapabilities->workspaceSymbolProvider = true; - // Support "Go to definition" - $serverCapabilities->definitionProvider = true; - // Support "Find all references" - $serverCapabilities->referencesProvider = true; - // Support "Hover" - $serverCapabilities->hoverProvider = true; - // Support "Completion" - $serverCapabilities->completionProvider = new CompletionOptions; - $serverCapabilities->completionProvider->resolveProvider = false; - $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; - - $serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(); - $serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ',']; - - // Support global references - $serverCapabilities->xworkspaceReferencesProvider = true; - $serverCapabilities->xdefinitionProvider = true; - $serverCapabilities->xdependenciesProvider = true; - - return new InitializeResult($serverCapabilities); }); } /** * 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 00000000..5ef9a277 --- /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 @@ public function testIndexingWithDirectFileAccess() $promise = new Promise; $input = new MockProtocolStream; $output = new MockProtocolStream; - $output->on('message', function (Message $msg) use ($promise) { - if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { + $output->on('message', function (Message $msg) use ($promise, $input) { + if ($msg->body->method === 'workspace/configuration') { + $result = new \stdClass(); + $result->fileTypes = ['.php']; + + $input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result]))); + } elseif ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->params->type === MessageType::ERROR) { $promise->reject(new Exception($msg->body->params->message)); } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { @@ -67,7 +72,8 @@ public function testIndexingWithDirectFileAccess() }); $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 @@ public function testIndexingWithFilesAndContentRequests() $output = new MockProtocolStream; $run = 1; $output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) { - if ($msg->body->method === 'textDocument/xcontent') { + if ($msg->body->method === 'workspace/configuration') { + $result = new \stdClass(); + $result->fileTypes = ['.php']; + + $input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result]))); + } elseif ($msg->body->method === 'textDocument/xcontent') { // Document content requested $contentCalled = true; $textDocumentItem = new TextDocumentItem; @@ -115,7 +126,8 @@ public function testIndexingWithFilesAndContentRequests() $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 @@ public function testIndexingMultipleFileTypes() $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 @@ public function testIndexingMultipleFileTypes() }); $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 5377c3a4..33b86e55 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -147,4 +147,14 @@ public function getReferenceUris(string $fqn): array } return $refs; } + + /** + * Wipe all indexes for a reindex + */ + public function wipe() + { + foreach ($this->getIndexes() as $index) { + $index->wipe(); + } + } } diff --git a/src/Index/Index.php b/src/Index/Index.php index 9cb975e5..2755ad70 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -222,4 +222,15 @@ public function serialize() 'staticComplete' => $this->staticComplete ]); } + + /** + * Clear indexed references and definitions + */ + public function wipe() + { + $this->definitions = []; + $this->references = []; + $this->complete = false; + $this->staticComplete = false; + } } diff --git a/src/Indexer.php b/src/Indexer.php index e4110543..253c7d7b 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 @@ public function __construct( $this->options = $options; $this->composerLock = $composerLock; $this->composerJson = $composerJson; + $this->hasCancellationSignal = false; + $this->isIndexing = false; } /** @@ -118,6 +130,7 @@ public function index(): Promise $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 @@ public function index(): Promise } } + $this->isIndexing = false; $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage( @@ -204,6 +218,34 @@ public function index(): Promise }); } + /** + * Return current indexing state + * + * @return bool + */ + public function isIndexing(): bool + { + return $this->isIndexing; + } + + /** + * Cancel all running indexing processes + * + * @return Promise + */ + public function cancel(): Promise + { + return coroutine(function () { + $this->hasCancellationSignal = true; + + while ($this->isIndexing()) { + yield timeout(); + } + + $this->hasCancellationSignal = false; + }); + } + /** * @param array $files * @return Promise @@ -212,6 +254,10 @@ private function indexFiles(array $files): Promise { 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 00822d77..29cfc4c9 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -329,6 +329,7 @@ public function initialized(): Promise $dependenciesIndex, $sourceIndex, $options, + $indexer, $this->composerLock, $this->documentLoader, $this->composerJson diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 548197b3..8eee5010 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -3,11 +3,12 @@ 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 @@ public function __construct(LanguageClient $client, ProjectIndex $projectIndex, $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 @@ public function symbol(string $query): Promise } /** - * The watched files notification is sent from the client to the server when the client detects changes to files watched by the language client. + * The watched files notification is sent from the client to the server when + * the client detects changes to files watched by the language client. * * @param FileEvent[] $changes * @return void @@ -113,7 +139,8 @@ public function didChangeWatchedFiles(array $changes) } /** - * The workspace references request is sent from the client to the server to locate project-wide references to a symbol given its description / metadata. + * The workspace references request is sent from the client to the server to + * locate project-wide references to a symbol given its description / metadata. * * @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for. * @param string[] $files An optional list of files to restrict the search to. @@ -174,4 +201,47 @@ public function xdependencies(): array } 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 45d949ff..27d64e03 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,9 +5,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{ - Server, LanguageClient, PhpDocumentLoader, DefinitionResolver -}; +use LanguageServer\{Options, Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range}; @@ -46,6 +44,7 @@ abstract class ServerTestCase extends TestCase public function setUp() { + $options = new Options(); $sourceIndex = new Index; $dependenciesIndex = new DependenciesIndex; $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); @@ -55,7 +54,7 @@ public function setUp() $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); - $this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader); + $this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $this->documentLoader); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); diff --git a/tests/Server/Workspace/DidChangeConfigurationTest.php b/tests/Server/Workspace/DidChangeConfigurationTest.php new file mode 100644 index 00000000..031852ed --- /dev/null +++ b/tests/Server/Workspace/DidChangeConfigurationTest.php @@ -0,0 +1,231 @@ +setComplete(); + + $rootPath = realpath(__DIR__ . '/../../../fixtures/'); + $filesFinder = new FileSystemFilesFinder; + $cache = new FileSystemCache; + $initialOptions = new Options; + + $input = new MockProtocolStream; + $output = new MockProtocolStream; + + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient($input, $output); + $documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex); + $indexer = new Indexer( + $filesFinder, + $rootPath, + $client, + $cache, + $dependenciesIndex, + $sourceIndex, + $documentLoader, + $initialOptions + ); + $workspace = new Server\Workspace( + $client, + $projectIndex, + $dependenciesIndex, + $sourceIndex, + $initialOptions, + null, + $documentLoader + ); + + $output->on('message', function (Message $msg) use ($promise) { + if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) { + $hasMessage = strpos( + $msg->body->params->message, + 'Settings could not be applied. For more information see logs.' + ) !== false; + + if ($msg->body->params->type === MessageType::ERROR && $hasMessage) { + $promise->fulfill(true); + } + + if ($msg->body->params->type !== MessageType::ERROR) { + $promise->reject(new Exception($msg->body->params->message)); + } + } + }); + + $settings = new \stdClass(); + $settings->php = new \stdClass(); + $settings->php->fileTypes = 'not an array'; + + $workspace->didChangeConfiguration($settings); + $this->assertTrue($promise->wait()); + } + + public function testNoChangedOptions() + { + $promise = new Promise; + $sourceIndex = new Index; + $dependenciesIndex = new DependenciesIndex; + $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $projectIndex->setComplete(); + + $rootPath = realpath(__DIR__ . '/../../../fixtures/'); + $filesFinder = new FileSystemFilesFinder; + $cache = new FileSystemCache; + $initialOptions = new Options; + + $input = new MockProtocolStream; + $output = new MockProtocolStream; + + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient($input, $output); + $documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex); + $indexer = new Indexer( + $filesFinder, + $rootPath, + $client, + $cache, + $dependenciesIndex, + $sourceIndex, + $documentLoader, + $initialOptions + ); + $workspace = new Server\Workspace( + $client, + $projectIndex, + $dependenciesIndex, + $sourceIndex, + $initialOptions, + null, + $documentLoader + ); + + $output->on('message', function (Message $msg) use ($promise) { + $promise->reject(new Exception($msg->body->message)); + }); + + $settings = new \stdClass(); + $settings->php = new \stdClass(); + $settings->php->fileTypes = ['.php']; + + $this->expectException(\LogicException::class); + $workspace->didChangeConfiguration($settings); + $promise->wait(); + } + + public function testDetectsChangedOptions() + { + $promise = new Promise; + $sourceIndex = new Index; + $dependenciesIndex = new DependenciesIndex; + $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); + $projectIndex->setComplete(); + + $rootPath = realpath(__DIR__ . '/../../../fixtures/'); + $filesFinder = new FileSystemFilesFinder; + $cache = new FileSystemCache; + $initialOptions = new Options; + + $input = new MockProtocolStream; + $output = new MockProtocolStream; + + $definitionResolver = new DefinitionResolver($projectIndex); + $client = new LanguageClient($input, $output); + $documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex); + $indexer = new Indexer( + $filesFinder, + $rootPath, + $client, + $cache, + $dependenciesIndex, + $sourceIndex, + $documentLoader, + $initialOptions + ); + $workspace = new Server\Workspace( + $client, + $projectIndex, + $dependenciesIndex, + $sourceIndex, + $initialOptions, + null, + $documentLoader + ); + + $output->on('message', function (Message $msg) use ($promise) { + if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) { + $hasMessage = strpos( + $msg->body->params->message, + 'You must restart your editor for the changes to take effect.' + ) !== false; + + if ($msg->body->params->type === MessageType::INFO && $hasMessage) { + $promise->fulfill(true); + } + + if ($msg->body->params->type === MessageType::ERROR) { + $promise->reject(new Exception($msg->body->params->message)); + } + } + }); + + $settings = new \stdClass(); + $settings->php = new \stdClass(); + $settings->php->fileTypes = ['.php', '.php5']; // default is only .php + + $workspace->didChangeConfiguration($settings); + $this->assertTrue($promise->wait()); + } +} diff --git a/tests/Server/Workspace/DidChangeWatchedFilesTest.php b/tests/Server/Workspace/DidChangeWatchedFilesTest.php index 1074c583..d93985c6 100644 --- a/tests/Server/Workspace/DidChangeWatchedFilesTest.php +++ b/tests/Server/Workspace/DidChangeWatchedFilesTest.php @@ -4,7 +4,7 @@ namespace LanguageServer\Tests\Server\Workspace; use LanguageServer\ContentRetriever\FileSystemContentRetriever; -use LanguageServer\{DefinitionResolver, LanguageClient, PhpDocumentLoader, Server}; +use LanguageServer\{DefinitionResolver, LanguageClient, Options, PhpDocumentLoader, Server}; use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex}; use LanguageServer\Protocol\{FileChangeType, FileEvent, Message}; use LanguageServer\Tests\MockProtocolStream; @@ -16,11 +16,12 @@ class DidChangeWatchedFilesTest extends ServerTestCase { public function testDeletingFileClearsAllDiagnostics() { + $options = new Options(); $client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream()); $projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex()); $definitionResolver = new DefinitionResolver($projectIndex); $loader = new PhpDocumentLoader(new FileSystemContentRetriever(), $projectIndex, $definitionResolver); - $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $loader, null); + $workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $loader, null); $fileEvent = new FileEvent('my uri', FileChangeType::DELETED); 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 | 110 +++++++++++-------- src/Protocol/ClientCapabilities.php | 5 + src/Protocol/WorkspaceClientCapabilities.php | 11 ++ src/Server/Workspace.php | 67 ++++++----- 5 files changed, 139 insertions(+), 74 deletions(-) create mode 100644 src/Protocol/WorkspaceClientCapabilities.php diff --git a/src/Indexer.php b/src/Indexer.php index 253c7d7b..93812b93 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 @@ public function __construct( DependenciesIndex $dependenciesIndex, Index $sourceIndex, PhpDocumentLoader $documentLoader, - Options $options, \stdClass $composerLock = null, \stdClass $composerJson = null ) { @@ -108,11 +106,24 @@ public function __construct( $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 @@ public function index(): Promise $this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references'); yield $this->indexFiles($source); $this->sourceIndex->setStaticComplete(); + // Dynamic references $this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references'); yield $this->indexFiles($source); @@ -243,6 +255,7 @@ public function cancel(): Promise } $this->hasCancellationSignal = false; + $this->client->window->logMessage(MessageType::INFO, 'Indexing project canceled'); }); } @@ -254,6 +267,7 @@ private function indexFiles(array $files): Promise { 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 29cfc4c9..4a04d73c 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 @@ public function initialize( $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 @@ public function initialize( } $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 @@ public function initialize( 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 - ); + if (!$this->rootPath) { + return; + } - $indexer->index()->otherwise('\\LanguageServer\\crash'); + // 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->textDocument === null) { - $this->textDocument = new Server\TextDocument( - $this->documentLoader, - $this->definitionResolver, - $this->client, - $this->globalIndex, - $this->composerJson, - $this->composerLock - ); + // 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 ($this->workspace === null) { - $this->workspace = new Server\Workspace( - $this->client, - $this->projectIndex, - $dependenciesIndex, - $sourceIndex, - $options, - $indexer, - $this->composerLock, - $this->documentLoader, - $this->composerJson - ); + 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 5228c7d9..c1ca7ea6 100644 --- a/src/Protocol/ClientCapabilities.php +++ b/src/Protocol/ClientCapabilities.php @@ -24,4 +24,9 @@ class ClientCapabilities * @var bool|null */ public $xcacheProvider; + + /** + * @var WorkspaceClientCapabilities + */ + public $workspace; } diff --git a/src/Protocol/WorkspaceClientCapabilities.php b/src/Protocol/WorkspaceClientCapabilities.php new file mode 100644 index 00000000..6b116859 --- /dev/null +++ b/src/Protocol/WorkspaceClientCapabilities.php @@ -0,0 +1,11 @@ +composerLock = $composerLock; $this->documentLoader = $documentLoader; $this->composerJson = $composerJson; - $this->options = $options; $this->indexer = $indexer; } @@ -205,36 +197,32 @@ public function xdependencies(): array /** * 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); - // @TODO: get changed settings and apply them - // @TODO: check settings that affect the indexer + if ($this->hasIndexerOptionsChanged($currentIndexerOptions, $options)) { + if ($this->indexer->isIndexing()) { + yield $this->indexer->cancel(); + } - if ($this->indexer->isIndexing()) { - yield $this->indexer->cancel(); + $this->projectIndex->wipe(); + $this->indexer->index()->otherwise('\\LanguageServer\\crash'); } - - $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 @@ public function didChangeConfiguration(\stdClass $settings): Promise } }); } + + /** + * 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 20573402..820fc459 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -56,7 +56,7 @@ public function xfiles(string $base = null): Promise * 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 4a04d73c..9548b7d5 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -341,9 +341,7 @@ public function initialized(): Promise // 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 5ef9a277..dff8b95f 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 ac8f92b8..61230e62 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -221,7 +221,7 @@ public function didChangeConfiguration($settings): Promise } $this->projectIndex->wipe(); - $this->indexer->index()->otherwise('\\LanguageServer\\crash'); + yield $this->indexer->index(); } } catch (\JsonMapper_Exception $exception) { $this->client->window->showMessage(