1
0
Fork 0

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.
pull/308/head
Jürgen Steitz 2018-08-31 11:54:52 +02:00
parent a5417cdf72
commit e317e8c743
5 changed files with 171 additions and 74 deletions

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\ConfigurationItem;
use LanguageServer\Protocol\TextDocumentIdentifier; use LanguageServer\Protocol\TextDocumentIdentifier;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;
@ -44,4 +45,24 @@ class Workspace
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class); 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]
);
}
} }

View File

@ -35,7 +35,7 @@ class ProjectIndex extends AbstractAggregateIndex
/** /**
* @return ReadableIndex[] * @return ReadableIndex[]
*/ */
protected function getIndexes(): array public function getIndexes(): array
{ {
return [$this->sourceIndex, $this->dependenciesIndex]; return [$this->sourceIndex, $this->dependenciesIndex];
} }

View File

@ -4,6 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
ConfigurationItem,
ServerCapabilities, ServerCapabilities,
ClientCapabilities, ClientCapabilities,
TextDocumentSyncKind, TextDocumentSyncKind,
@ -15,7 +16,7 @@ use LanguageServer\Protocol\{
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache}; use LanguageServer\Cache\{Cache, FileSystemCache, ClientCache};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
@ -106,6 +107,16 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
*/ */
protected $definitionResolver; protected $definitionResolver;
/**
* @var string|null
*/
protected $rootPath;
/**
* @var Cache
*/
protected $cache;
/** /**
* @param ProtocolReader $reader * @param ProtocolReader $reader
* @param ProtocolWriter $writer * @param ProtocolWriter $writer
@ -162,14 +173,18 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* *
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @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 string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. * @param int|null $processId The process Id of the parent process that started the server.
* @param Options $initializationOptions The options send from client to initialize 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 <InitializeResult> * @return Promise <InitializeResult>
*/ */
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, Options $initializationOptions = null): Promise public function initialize(
{ ClientCapabilities $capabilities,
return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) { string $rootPath = null,
int $processId = null
): Promise {
return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) { if ($capabilities->xfilesProvider) {
$this->filesFinder = new ClientFilesFinder($this->client); $this->filesFinder = new ClientFilesFinder($this->client);
} else { } else {
@ -187,82 +202,48 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson); $this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson);
$stubsIndex = StubsIndex::read(); $stubsIndex = StubsIndex::read();
$this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex); $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 // The DefinitionResolver should look in stubs, the project source and dependencies
$this->definitionResolver = new DefinitionResolver($this->globalIndex); $this->definitionResolver = new DefinitionResolver($this->globalIndex);
$this->documentLoader = new PhpDocumentLoader( $this->documentLoader = new PhpDocumentLoader(
$this->contentRetriever, $this->contentRetriever,
$this->projectIndex, $this->projectIndex,
$this->definitionResolver $this->definitionResolver
); );
if ($rootPath !== null) { if ($this->rootPath !== null) {
yield $this->beforeIndex($rootPath); yield $this->beforeIndex($this->rootPath);
// Find composer.json // Find composer.json
if ($this->composerJson === null) { 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); sortUrisLevelOrder($composerJsonFiles);
if (!empty($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 // Find composer.lock
if ($this->composerLock === null) { 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); sortUrisLevelOrder($composerLockFiles);
if (!empty($composerLockFiles)) { if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0])); $this->composerLock = json_decode(
} yield $this->contentRetriever->retrieve($composerLockFiles[0])
}
$cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
// Index in background
$indexer = new Indexer(
$this->filesFinder,
$rootPath,
$this->client,
$cache,
$dependenciesIndex,
$sourceIndex,
$this->documentLoader,
$initializationOptions,
$this->composerLock,
$this->composerJson,
$initializationOptions
);
$indexer->index()->otherwise('\\LanguageServer\\crash');
}
if ($this->textDocument === null) {
$this->textDocument = new Server\TextDocument(
$this->documentLoader,
$this->definitionResolver,
$this->client,
$this->globalIndex,
$this->composerJson,
$this->composerLock
); );
} }
if ($this->workspace === null) { }
$this->workspace = new Server\Workspace(
$this->client, $this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
$this->projectIndex,
$dependenciesIndex,
$sourceIndex,
$this->composerLock,
$this->documentLoader,
$this->composerJson,
$indexer,
$initializationOptions
);
} }
$serverCapabilities = new ServerCapabilities(); $serverCapabilities = new ServerCapabilities();
@ -295,10 +276,71 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
}); });
} }
/**
* The initialized notification is sent from the client to the server after
* the client received the result of the initialize request but before the
* client is sending any other request or notification to the server.
*
* @return Promise
*/
public function initialized(): Promise
{
return coroutine(function () {
list($sourceIndex, $dependenciesIndex) = $this->projectIndex->getIndexes();
$mapper = new \JsonMapper();
$configurationitem = new ConfigurationItem();
$configurationitem->section = 'php';
$configuration = yield $this->client->workspace->configuration([$configurationitem]);
$options = $mapper->map($configuration[0], new Options());
if ($this->rootPath) {
// Index in background
$indexer = new Indexer(
$this->filesFinder,
$this->rootPath,
$this->client,
$this->cache,
$dependenciesIndex,
$sourceIndex,
$this->documentLoader,
$options,
$this->composerLock,
$this->composerJson
);
$indexer->index()->otherwise('\\LanguageServer\\crash');
}
if ($this->textDocument === null) {
$this->textDocument = new Server\TextDocument(
$this->documentLoader,
$this->definitionResolver,
$this->client,
$this->globalIndex,
$this->composerJson,
$this->composerLock
);
}
if ($this->workspace === null) {
$this->workspace = new Server\Workspace(
$this->client,
$this->projectIndex,
$dependenciesIndex,
$sourceIndex,
$options,
$this->composerLock,
$this->documentLoader,
$this->composerJson
);
}
});
}
/** /**
* The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit * 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 * (otherwise the response might not be delivered correctly to the client). There is a separate exit notification
* asks the server to exit. * that asks the server to exit.
* *
* @return void * @return void
*/ */

View File

@ -0,0 +1,20 @@
<?php
namespace LanguageServer\Protocol;
class ConfigurationItem
{
/**
* The scope to get the configuration section for.
*
* @var string|null
*/
public $scopeUri;
/**
* The configuration section asked for.
*
* @var string|null
*/
public $section;
}

View File

@ -30,7 +30,7 @@ class LanguageServerTest extends TestCase
public function testInitialize() public function testInitialize()
{ {
$server = new LanguageServer(new MockProtocolStream, new MockProtocolStream); $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream);
$result = $server->initialize(new ClientCapabilities, __DIR__, getmypid(), new Options)->wait(); $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait();
$serverCapabilities = new ServerCapabilities(); $serverCapabilities = new ServerCapabilities();
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
@ -56,8 +56,13 @@ class LanguageServerTest extends TestCase
$promise = new Promise; $promise = new Promise;
$input = new MockProtocolStream; $input = new MockProtocolStream;
$output = new MockProtocolStream; $output = new MockProtocolStream;
$output->on('message', function (Message $msg) use ($promise) { $output->on('message', function (Message $msg) use ($promise, $input) {
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { 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) { if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
@ -67,7 +72,8 @@ class LanguageServerTest extends TestCase
}); });
$server = new LanguageServer($input, $output); $server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities; $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()); $this->assertTrue($promise->wait());
} }
@ -81,7 +87,12 @@ class LanguageServerTest extends TestCase
$output = new MockProtocolStream; $output = new MockProtocolStream;
$run = 1; $run = 1;
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) { $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 // Document content requested
$contentCalled = true; $contentCalled = true;
$textDocumentItem = new TextDocumentItem; $textDocumentItem = new TextDocumentItem;
@ -115,7 +126,8 @@ class LanguageServerTest extends TestCase
$capabilities = new ClientCapabilities; $capabilities = new ClientCapabilities;
$capabilities->xfilesProvider = true; $capabilities->xfilesProvider = true;
$capabilities->xcontentProvider = true; $capabilities->xcontentProvider = true;
$server->initialize($capabilities, $rootPath, getmypid(), new Options); $server->initialize($capabilities, $rootPath, getmypid())->wait();
$server->initialized();
$promise->wait(); $promise->wait();
$this->assertTrue($filesCalled); $this->assertTrue($filesCalled);
$this->assertTrue($contentCalled); $this->assertTrue($contentCalled);
@ -126,13 +138,14 @@ class LanguageServerTest extends TestCase
$promise = new Promise; $promise = new Promise;
$input = new MockProtocolStream; $input = new MockProtocolStream;
$output = new MockProtocolStream; $output = new MockProtocolStream;
$options = new Options;
$options->setFileTypes([ $output->on('message', function (Message $msg) use ($promise, $input) {
'.php', if ($msg->body->method === 'workspace/configuration') {
'.inc' $result = new \stdClass();
]); $result->fileTypes = ['.php', '.inc'];
$output->on('message', function (Message $msg) use ($promise, &$allFilesParsed) {
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { $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) { if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} elseif (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) { } elseif (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
@ -142,7 +155,8 @@ class LanguageServerTest extends TestCase
}); });
$server = new LanguageServer($input, $output); $server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities; $capabilities = new ClientCapabilities;
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), $options); $server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid());
$server->initialized();
$this->assertTrue($promise->wait()); $this->assertTrue($promise->wait());
} }
} }