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()); } }