From 9a65f2a8727efad97a9139c552f6e2d04decc007 Mon Sep 17 00:00:00 2001 From: Robert Lu Date: Wed, 19 Jun 2019 19:02:46 +0800 Subject: [PATCH] move to amphp --- bin/php-language-server.php | 102 ++++---- composer.json | 8 +- src/Cache/Cache.php | 14 +- src/Cache/ClientCache.php | 18 +- src/Cache/FileSystemCache.php | 38 ++- src/Client/TextDocument.php | 15 +- src/Client/Window.php | 4 +- src/Client/Workspace.php | 11 +- src/Client/XCache.php | 12 +- src/ClientHandler.php | 46 ++-- src/ComposerScripts.php | 24 +- .../ClientContentRetriever.php | 11 +- src/ContentRetriever/ContentRetriever.php | 2 +- .../FileSystemContentRetriever.php | 4 +- src/Event/MessageEvent.php | 32 +++ src/FilesFinder/ClientFilesFinder.php | 26 +- src/FilesFinder/FileSystemFilesFinder.php | 36 +-- src/FilesFinder/FilesFinder.php | 4 +- src/Index/AbstractAggregateIndex.php | 12 +- src/Index/DependenciesIndex.php | 7 + src/Index/Index.php | 13 +- src/Index/ReadableIndex.php | 2 +- src/Indexer.php | 225 +++++++++--------- src/LanguageServer.php | 109 ++++----- src/Message.php | 9 +- src/PhpDocument.php | 2 +- src/PhpDocumentLoader.php | 61 +++-- src/ProtocolReader.php | 4 +- src/ProtocolStreamReader.php | 83 +++---- src/ProtocolStreamWriter.php | 25 +- src/ProtocolWriter.php | 2 +- src/Server/TextDocument.php | 148 +++++++----- src/Server/Workspace.php | 83 +++---- src/SignatureHelpProvider.php | 155 ++++++------ src/utils.php | 46 +--- 35 files changed, 678 insertions(+), 715 deletions(-) create mode 100644 src/Event/MessageEvent.php diff --git a/bin/php-language-server.php b/bin/php-language-server.php index 8e5d348..24ead3b 100644 --- a/bin/php-language-server.php +++ b/bin/php-language-server.php @@ -1,8 +1,12 @@ critical("Could not connect to language client. Error $errno\n$errstr"); - exit(1); - } - stream_set_blocking($socket, false); - $ls = new LanguageServer( - new ProtocolStreamReader($socket), - new ProtocolStreamWriter($socket) - ); - Loop\run(); + $server = function () use ($logger, $address) { + /** @var ClientSocket $socket */ + $socket = yield Amp\Socket\connect('tcp://' . $address); + $ls = new LanguageServer( + new ProtocolStreamReader($socket), + new ProtocolStreamWriter($socket) + ); + yield $ls->getshutdownDeferred(); + }; } else if (!empty($options['tcp-server'])) { // Run a TCP Server $address = $options['tcp-server']; - $tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr); - if ($tcpServer === false) { - $logger->critical("Could not listen on $address. Error $errno\n$errstr"); - exit(1); - } - $logger->debug("Server listening on $address"); - $pcntlAvailable = extension_loaded('pcntl'); - if (!$pcntlAvailable) { - $logger->notice('PCNTL is not available. Only a single connection will be accepted'); - } - while ($socket = stream_socket_accept($tcpServer, -1)) { - $logger->debug('Connection accepted'); - stream_set_blocking($socket, false); - if ($pcntlAvailable) { - // If PCNTL is available, fork a child process for the connection - // An exit notification will only terminate the child process - $pid = pcntl_fork(); - if ($pid === -1) { - $logger->critical('Could not fork'); - exit(1); - } else if ($pid === 0) { - // Child process - $reader = new ProtocolStreamReader($socket); - $writer = new ProtocolStreamWriter($socket); - $reader->on('close', function () use ($logger) { - $logger->debug('Connection closed'); - }); - $ls = new LanguageServer($reader, $writer); - Loop\run(); - // Just for safety - exit(0); - } - } else { - // If PCNTL is not available, we only accept one connection. - // An exit notification will terminate the server - $ls = new LanguageServer( - new ProtocolStreamReader($socket), - new ProtocolStreamWriter($socket) - ); - Loop\run(); + $server = function () use ($logger, $address) { + + $server = Amp\Socket\listen('tcp://' . $address); + + $logger->debug("Server listening on $address"); + + while ($socket = yield $server->accept()) { + /** @var ServerSocket $socket */ + list($ip, $port) = \explode(':', $socket->getRemoteAddress()); + + $logger->debug("Accepted connection from {$ip}:{$port}." . PHP_EOL); + + Loop::run(function () use ($socket) { + $ls = new LanguageServer( + new ProtocolStreamReader($socket), + new ProtocolStreamWriter($socket) + ); + yield $ls->getshutdownDeferred(); + }); } - } + }; } else { // Use STDIO $logger->debug('Listening on STDIN'); - stream_set_blocking(STDIN, false); + $inputStream = new ResourceInputStream(STDIN); + $outputStream = new ResourceOutputStream(STDOUT); $ls = new LanguageServer( - new ProtocolStreamReader(STDIN), - new ProtocolStreamWriter(STDOUT) + new ProtocolStreamReader($inputStream), + new ProtocolStreamWriter($outputStream) ); - Loop\run(); + $server = function () use ($ls) { + yield $ls->getshutdownDeferred(); + }; } + +Loop::run($server); diff --git a/composer.json b/composer.json index a57f98b..4d5e6ae 100644 --- a/composer.json +++ b/composer.json @@ -22,17 +22,21 @@ ], "require": { "php": "^7.0", + "amphp/byte-stream": "^1.5", + "amphp/cache": "^1.2", + "amphp/file": "^0.3.5", + "amphp/socket": "^0.10.11", "composer/xdebug-handler": "^1.0", "felixfbecker/advanced-json-rpc": "^3.0.0", "felixfbecker/language-server-protocol": "^1.0.1", "jetbrains/phpstorm-stubs": "dev-master", + "league/event": "^2.2", + "league/uri-parser": "^1.4", "microsoft/tolerant-php-parser": "0.0.*", "netresearch/jsonmapper": "^1.0", "php-ds/php-ds": "^1.2", "phpdocumentor/reflection-docblock": "^4.0.0", "psr/log": "^1.0", - "sabre/event": "^5.0", - "sabre/uri": "^2.0", "webmozart/glob": "^4.1", "webmozart/path-util": "^2.3" }, diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index ebc5233..2705e3e 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -1,10 +1,8 @@ + * @return \Generator */ - public function get(string $key): Promise; + public function get(string $key): \Generator; /** * Sets a value in the cache * * @param string $key - * @param mixed $value - * @return Promise + * @param mixed $value + * @return \Generator */ - public function set(string $key, $value): Promise; + public function set(string $key, $value): \Generator; } diff --git a/src/Cache/ClientCache.php b/src/Cache/ClientCache.php index e92a3d5..f7b885e 100644 --- a/src/Cache/ClientCache.php +++ b/src/Cache/ClientCache.php @@ -1,5 +1,5 @@ */ - public function get(string $key): Promise + public function get(string $key): \Generator { - return $this->client->xcache->get($key)->then('unserialize')->otherwise(function () { - // Ignore - }); + $cached = yield from $this->client->xcache->get($key); + $obj = unserialize($cached); + return $obj; } /** * Sets a value in the cache * * @param string $key - * @param mixed $value + * @param mixed $value * @return Promise */ - public function set(string $key, $value): Promise + public function set(string $key, $value): \Generator { - return $this->client->xcache->set($key, serialize($value))->otherwise(function () { - // Ignore - }); + return yield from $this->client->xcache->set($key, serialize($value)); } } diff --git a/src/Cache/FileSystemCache.php b/src/Cache/FileSystemCache.php index d6d4410..3856804 100644 --- a/src/Cache/FileSystemCache.php +++ b/src/Cache/FileSystemCache.php @@ -1,10 +1,8 @@ + * @return \Generator */ - public function get(string $key): Promise + public function get(string $key): \Generator { try { $file = $this->cacheDir . urlencode($key); - if (!file_exists($file)) { - return Promise\resolve(null); - } - return Promise\resolve(unserialize(file_get_contents($file))); + $content = yield \Amp\File\get($file); + return unserialize($content); } catch (\Exception $e) { - return Promise\resolve(null); + return null; } } @@ -49,19 +45,19 @@ class FileSystemCache implements Cache * Sets a value in the cache * * @param string $key - * @param mixed $value - * @return Promise + * @param mixed $value + * @return \Generator */ - public function set(string $key, $value): Promise + public function set(string $key, $value): \Generator { - try { - $file = $this->cacheDir . urlencode($key); - if (!file_exists($this->cacheDir)) { - mkdir($this->cacheDir); - } - file_put_contents($file, serialize($value)); - } finally { - return Promise\resolve(null); + $file = $this->cacheDir . urlencode($key); + $dir = dirname($file); + if (yield \Amp\File\isfile($dir)) { + yield \Amp\File\unlink($dir); } + if (!yield \Amp\File\exists($dir)) { + yield \Amp\File\mkdir($dir, 0777, true); + } + yield \Amp\File\put($file, serialize($value)); } } diff --git a/src/Client/TextDocument.php b/src/Client/TextDocument.php index 03211fa..7ae8e4b 100644 --- a/src/Client/TextDocument.php +++ b/src/Client/TextDocument.php @@ -1,5 +1,5 @@ */ - public function publishDiagnostics(string $uri, array $diagnostics): Promise + public function publishDiagnostics(string $uri, array $diagnostics): \Generator { - return $this->handler->notify('textDocument/publishDiagnostics', [ + yield from $this->handler->notify('textDocument/publishDiagnostics', [ 'uri' => $uri, 'diagnostics' => $diagnostics ]); @@ -51,13 +51,12 @@ class TextDocument * @param TextDocumentIdentifier $textDocument The document to get the content for * @return Promise The document's current content */ - public function xcontent(TextDocumentIdentifier $textDocument): Promise + public function xcontent(TextDocumentIdentifier $textDocument): \Generator { - return $this->handler->request( + $result = yield from $this->handler->request( 'textDocument/xcontent', ['textDocument' => $textDocument] - )->then(function ($result) { - return $this->mapper->map($result, new TextDocumentItem); - }); + ); + return $this->mapper->map($result, new TextDocumentItem); } } diff --git a/src/Client/Window.php b/src/Client/Window.php index c3558f5..14ac73e 100644 --- a/src/Client/Window.php +++ b/src/Client/Window.php @@ -41,8 +41,8 @@ class Window * @param string $message * @return Promise */ - public function logMessage(int $type, string $message): Promise + public function logMessage(int $type, string $message): \Generator { - return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]); + yield from $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]); } } diff --git a/src/Client/Workspace.php b/src/Client/Workspace.php index 8c31f1f..bd4b403 100644 --- a/src/Client/Workspace.php +++ b/src/Client/Workspace.php @@ -1,5 +1,5 @@ Array of documents */ - public function xfiles(string $base = null): Promise + public function xfiles(string $base = null): \Generator { - return $this->handler->request( + $textDocuments = yield from $this->handler->request( 'workspace/xfiles', ['base' => $base] - )->then(function (array $textDocuments) { - return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class); - }); + ); + return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class); } } diff --git a/src/Client/XCache.php b/src/Client/XCache.php index b3ce5a5..fa23fbd 100644 --- a/src/Client/XCache.php +++ b/src/Client/XCache.php @@ -1,5 +1,5 @@ */ - public function get(string $key): Promise + public function get(string $key): \Generator { - return $this->handler->request('xcache/get', ['key' => $key]); + return yield from $this->handler->request('xcache/get', ['key' => $key]); } /** * @param string $key - * @param mixed $value + * @param mixed $value * @return Promise */ - public function set(string $key, $value): Promise + public function set(string $key, $value): \Generator { - return $this->handler->notify('xcache/set', ['key' => $key, 'value' => $value]); + return yield from $this->handler->notify('xcache/set', ['key' => $key, 'value' => $value]); } } diff --git a/src/ClientHandler.php b/src/ClientHandler.php index c98cbc6..094a292 100644 --- a/src/ClientHandler.php +++ b/src/ClientHandler.php @@ -1,10 +1,12 @@ Resolved with the result of the request or rejected with an error + * @return \Generator Resolved with the result of the request or rejected with an error */ - public function request(string $method, $params): Promise + public function request(string $method, $params): \Generator { $id = $this->idGenerator->generate(); - return $this->protocolWriter->write( - new Message( - new AdvancedJsonRpc\Request($id, $method, (object)$params) - ) - )->then(function () use ($id) { - $promise = new Promise; - $listener = function (Message $msg) use ($id, $promise, &$listener) { + $deferred = new Deferred(); + $listener = function (MessageEvent $messageEvent) use ($id, $deferred, &$listener) { + $msg = $messageEvent->getMessage(); + Loop::defer(function () use (&$listener, $deferred, $id, $msg) { if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) { // Received a response $this->protocolReader->removeListener('message', $listener); if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) { - $promise->fulfill($msg->body->result); + $deferred->resolve($msg->body->result); } else { - $promise->reject($msg->body->error); + $deferred->fail($msg->body->error); } } - }; - $this->protocolReader->on('message', $listener); - return $promise; - }); + }); + }; + $this->protocolReader->addListener('message', $listener); + + yield from $this->protocolWriter->write( + new Message( + new AdvancedJsonRpc\Request($id, $method, (object)$params) + ) + ); + + return yield $deferred->promise(); } /** @@ -67,11 +73,11 @@ class ClientHandler * * @param string $method The method to call * @param array|object $params The method parameters - * @return Promise Will be resolved as soon as the notification has been sent + * @return \Generator Will be resolved as soon as the notification has been sent */ - public function notify(string $method, $params): Promise + public function notify(string $method, $params): \Generator { - return $this->protocolWriter->write( + return yield from $this->protocolWriter->write( new Message( new AdvancedJsonRpc\Notification($method, (object)$params) ) diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php index 2a584d2..7535f74 100644 --- a/src/ComposerScripts.php +++ b/src/ComposerScripts.php @@ -1,16 +1,16 @@ find("$stubsLocation/**/*.php"); + $uris = yield from $finder->find("$stubsLocation/**/*.php"); foreach ($uris as $uri) { echo "Parsing $uri\n"; - $content = yield $contentRetriever->retrieve($uri); + $content = yield from $contentRetriever->retrieve($uri); // Change URI to phpstubs:// - $parts = Uri\parse($uri); + $parts = parse($uri); $parts['path'] = Path::makeRelative($parts['path'], $stubsLocation); $parts['scheme'] = 'phpstubs'; - $uri = Uri\build($parts); // Create a new document and add it to $index - new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + new PhpDocument((string)$uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); } $index->setComplete(); @@ -67,6 +65,6 @@ class ComposerScripts $index->save(); echo "Finished\n"; - })->wait(); + }); } } diff --git a/src/ContentRetriever/ClientContentRetriever.php b/src/ContentRetriever/ClientContentRetriever.php index d83aad1..a09bb2f 100644 --- a/src/ContentRetriever/ClientContentRetriever.php +++ b/src/ContentRetriever/ClientContentRetriever.php @@ -1,5 +1,5 @@ Resolved with the content as a string */ - public function retrieve(string $uri): Promise + public function retrieve(string $uri): \Generator { - return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)) - ->then(function (TextDocumentItem $textDocument) { - return $textDocument->text; - }); + /** @var TextDocumentItem $textDocument */ + $textDocument = yield from $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)); + return $textDocument->text; } } diff --git a/src/ContentRetriever/ContentRetriever.php b/src/ContentRetriever/ContentRetriever.php index 4d16b98..9d926fb 100644 --- a/src/ContentRetriever/ContentRetriever.php +++ b/src/ContentRetriever/ContentRetriever.php @@ -16,5 +16,5 @@ interface ContentRetriever * @param string $uri The URI of the document * @return Promise Resolved with the content as a string */ - public function retrieve(string $uri): Promise; + public function retrieve(string $uri): \Generator; } diff --git a/src/ContentRetriever/FileSystemContentRetriever.php b/src/ContentRetriever/FileSystemContentRetriever.php index 82e7002..68b8152 100644 --- a/src/ContentRetriever/FileSystemContentRetriever.php +++ b/src/ContentRetriever/FileSystemContentRetriever.php @@ -17,8 +17,8 @@ class FileSystemContentRetriever implements ContentRetriever * @param string $uri The URI of the document * @return Promise Resolved with the content as a string */ - public function retrieve(string $uri): Promise + public function retrieve(string $uri): \Generator { - return Promise\resolve(file_get_contents(uriToPath($uri))); + return yield \Amp\File\get(uriToPath($uri)); } } diff --git a/src/Event/MessageEvent.php b/src/Event/MessageEvent.php new file mode 100644 index 0000000..3bfbe83 --- /dev/null +++ b/src/Event/MessageEvent.php @@ -0,0 +1,32 @@ +message = $message; + } + + public function getMessage(): Message + { + return $this->message; + } +} diff --git a/src/FilesFinder/ClientFilesFinder.php b/src/FilesFinder/ClientFilesFinder.php index 4315ede..8295bf1 100644 --- a/src/FilesFinder/ClientFilesFinder.php +++ b/src/FilesFinder/ClientFilesFinder.php @@ -1,12 +1,11 @@ The URIs + * @return \Generator The URIs */ - public function find(string $glob): Promise + public function find(string $glob): \Generator { - return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) { - $uris = []; - foreach ($textDocuments as $textDocument) { - $path = Uri\parse($textDocument->uri)['path']; - if (Glob::match($path, $glob)) { - $uris[] = $textDocument->uri; - } + $textDocuments = yield from $this->client->workspace->xfiles(); + $uris = []; + foreach ($textDocuments as $textDocument) { + $path = parse($textDocument->uri)['path']; + if (Glob::match($path, $glob)) { + $uris[] = $textDocument->uri; } - return $uris; - }); + } + return $uris; } } diff --git a/src/FilesFinder/FileSystemFilesFinder.php b/src/FilesFinder/FileSystemFilesFinder.php index a26b5d8..3bf24e0 100644 --- a/src/FilesFinder/FileSystemFilesFinder.php +++ b/src/FilesFinder/FileSystemFilesFinder.php @@ -1,12 +1,11 @@ + * @return \Amp\Promise */ - public function find(string $glob): Promise + public function find(string $glob): \Generator { - return coroutine(function () use ($glob) { - $uris = []; - foreach (new GlobIterator($glob) as $path) { - // Exclude any directories that also match the glob pattern - if (!is_dir($path)) { - $uris[] = pathToUri($path); + $uris = []; + $basePath = \Webmozart\Glob\Glob::getBasePath($glob); + $pathList = [$basePath]; + while ($pathList) { + $path = array_pop($pathList); + if (yield isdir($path)) { + $subFileList = yield \Amp\File\scandir($path); + foreach ($subFileList as $subFile) { + $pathList[] = $path . DIRECTORY_SEPARATOR . $subFile; } - - yield timeout(); + } elseif (Glob::match($path, $glob)) { + $uris[] = pathToUri($path); } - return $uris; - }); + } + return $uris; } } diff --git a/src/FilesFinder/FilesFinder.php b/src/FilesFinder/FilesFinder.php index 81d6de5..159dc35 100644 --- a/src/FilesFinder/FilesFinder.php +++ b/src/FilesFinder/FilesFinder.php @@ -1,5 +1,5 @@ */ - public function find(string $glob): Promise; + public function find(string $glob): \Generator; } diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php index 8c8c95a..9c4ee53 100644 --- a/src/Index/AbstractAggregateIndex.php +++ b/src/Index/AbstractAggregateIndex.php @@ -4,12 +4,10 @@ declare(strict_types = 1); namespace LanguageServer\Index; use LanguageServer\Definition; -use Sabre\Event\EmitterTrait; +use League\Event\Emitter; -abstract class AbstractAggregateIndex implements ReadableIndex +abstract class AbstractAggregateIndex extends Emitter implements ReadableIndex { - use EmitterTrait; - /** * Returns all indexes managed by the aggregate index * @@ -29,17 +27,17 @@ abstract class AbstractAggregateIndex implements ReadableIndex */ protected function registerIndex(ReadableIndex $index) { - $index->on('complete', function () { + $index->addListener('complete', function () { if ($this->isComplete()) { $this->emit('complete'); } }); - $index->on('static-complete', function () { + $index->addListener('static-complete', function () { if ($this->isStaticComplete()) { $this->emit('static-complete'); } }); - $index->on('definition-added', function () { + $index->addListener('definition-added', function () { $this->emit('definition-added'); }); } diff --git a/src/Index/DependenciesIndex.php b/src/Index/DependenciesIndex.php index 059cb7d..8bd0ff8 100644 --- a/src/Index/DependenciesIndex.php +++ b/src/Index/DependenciesIndex.php @@ -3,6 +3,13 @@ declare(strict_types = 1); namespace LanguageServer\Index; +use League\Event\EmitterInterface; +use League\Event\EventInterface; +use League\Event\GeneratorInterface; +use League\Event\ListenerAcceptorInterface; +use League\Event\ListenerInterface; +use League\Event\ListenerProviderInterface; + class DependenciesIndex extends AbstractAggregateIndex { /** diff --git a/src/Index/Index.php b/src/Index/Index.php index 351ac02..07d831e 100644 --- a/src/Index/Index.php +++ b/src/Index/Index.php @@ -5,15 +5,14 @@ namespace LanguageServer\Index; use Ds\Set; use LanguageServer\Definition; -use Sabre\Event\EmitterTrait; +use League\Event\Emitter; /** * Represents the index of a project or dependency * Serializable for caching */ -class Index implements ReadableIndex, \Serializable +class Index extends Emitter implements ReadableIndex, \Serializable { - use EmitterTrait; /** * An associative array that maps splitted fully qualified symbol names @@ -62,7 +61,6 @@ class Index implements ReadableIndex, \Serializable $this->setStaticComplete(); } $this->complete = true; - $this->emit('complete'); } /** @@ -73,7 +71,6 @@ class Index implements ReadableIndex, \Serializable public function setStaticComplete() { $this->staticComplete = true; - $this->emit('static-complete'); } /** @@ -174,8 +171,6 @@ class Index implements ReadableIndex, \Serializable { $parts = $this->splitFqn($fqn); $this->indexDefinition(0, $parts, $this->definitions, $definition); - - $this->emit('definition-added'); } /** @@ -201,7 +196,7 @@ class Index implements ReadableIndex, \Serializable */ public function getReferenceUris(string $fqn): \Generator { - if ($this->references[$fqn]) { + if (isset($this->references[$fqn])) { foreach ($this->references[$fqn] as $uri) { yield $uri; } @@ -425,7 +420,7 @@ class Index implements ReadableIndex, \Serializable if (isset($storage[$part])) { unset($storage[$part]); - if (0 === count($storage)) { + if (0 === count($storage) && $level != 0) { // parse again the definition tree to remove the parent // when it has no more children $this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage); diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php index 505bb9a..8f68a70 100644 --- a/src/Index/ReadableIndex.php +++ b/src/Index/ReadableIndex.php @@ -4,7 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Index; use LanguageServer\Definition; -use Sabre\Event\EmitterInterface; +use League\Event\EmitterInterface; /** * The ReadableIndex interface provides methods to lookup definitions and references diff --git a/src/Indexer.php b/src/Indexer.php index 7ebff3f..b13cc78 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -1,15 +1,15 @@ */ - public function index(): Promise + public function index(): \Generator { - return coroutine(function () { + $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); + $uris = yield from $this->filesFinder->find($pattern); - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); - $uris = yield $this->filesFinder->find($pattern); + $count = count($uris); + $startTime = microtime(true); + yield from $this->client->window->logMessage(MessageType::INFO, "$count files total"); - $count = count($uris); - $startTime = microtime(true); - $this->client->window->logMessage(MessageType::INFO, "$count files total"); + /** @var string[] */ + $source = []; + /** @var string[][] */ + $deps = []; - /** @var string[] */ - $source = []; - /** @var string[][] */ - $deps = []; + foreach ($uris as $uri) { + $packageName = getPackageName($uri, $this->composerJson); + if ($this->composerLock !== null && $packageName) { + // Dependency file + if (!isset($deps[$packageName])) { + $deps[$packageName] = []; + } + $deps[$packageName][] = $uri; + } else { + // Source file + $source[] = $uri; + } + } - foreach ($uris as $uri) { - $packageName = getPackageName($uri, $this->composerJson); - if ($this->composerLock !== null && $packageName) { - // Dependency file - if (!isset($deps[$packageName])) { - $deps[$packageName] = []; - } - $deps[$packageName][] = $uri; - } else { - // Source file - $source[] = $uri; + // Index source + // Definitions and static references + yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references'); + yield from $this->indexFiles($source); + $this->sourceIndex->setStaticComplete(); + // Dynamic references + yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references'); + yield from $this->indexFiles($source); + $this->sourceIndex->setComplete(); + + // Index dependencies + yield from $this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages'); + foreach ($deps as $packageName => $files) { + // Find version of package and check cache + $packageKey = null; + $cacheKey = null; + $index = null; + foreach (array_merge($this->composerLock->packages, (array)$this->composerLock->{'packages-dev'}) as $package) { + // Check if package name matches and version is absolute + // Dynamic constraints are not cached, because they can change every time + $packageVersion = ltrim($package->version, 'v'); + if ($package->name === $packageName && strpos($packageVersion, 'dev') === false) { + $packageKey = $packageName . ':' . $packageVersion; + $cacheKey = self::CACHE_VERSION . ':' . $packageKey; + // Check cache + $index = yield from $this->cache->get($cacheKey); + break; } } + $index = null; + if ($index !== null) { + // Cache hit + $this->dependenciesIndex->setDependencyIndex($packageName, $index); + yield from $this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache"); + } else { + // Cache miss + $index = $this->dependenciesIndex->getDependencyIndex($packageName); - // Index source - // Definitions and static references - $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); - $this->sourceIndex->setComplete(); + // Index definitions and static references + yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references'); + yield from $this->indexFiles($files); + $index->setStaticComplete(); - // Index dependencies - $this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages'); - foreach ($deps as $packageName => $files) { - // Find version of package and check cache - $packageKey = null; - $cacheKey = null; - $index = null; - foreach (array_merge($this->composerLock->packages, (array)$this->composerLock->{'packages-dev'}) as $package) { - // Check if package name matches and version is absolute - // Dynamic constraints are not cached, because they can change every time - $packageVersion = ltrim($package->version, 'v'); - if ($package->name === $packageName && strpos($packageVersion, 'dev') === false) { - $packageKey = $packageName . ':' . $packageVersion; - $cacheKey = self::CACHE_VERSION . ':' . $packageKey; - // Check cache - $index = yield $this->cache->get($cacheKey); - break; - } - } - if ($index !== null) { - // Cache hit - $this->dependenciesIndex->setDependencyIndex($packageName, $index); - $this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache"); + // Index dynamic references + yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references'); + yield from $this->indexFiles($files); + $index->setComplete(); + + // If we know the version (cache key), save index for the dependency in the cache + if ($cacheKey !== null) { + yield from $this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache"); + yield from $this->cache->set($cacheKey, $index); } else { - // Cache miss - $index = $this->dependenciesIndex->getDependencyIndex($packageName); - - // Index definitions and static references - $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references'); - yield $this->indexFiles($files); - $index->setStaticComplete(); - - // Index dynamic references - $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references'); - yield $this->indexFiles($files); - $index->setComplete(); - - // If we know the version (cache key), save index for the dependency in the cache - if ($cacheKey !== null) { - $this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache"); - $this->cache->set($cacheKey, $index); - } else { - $this->client->window->logMessage(MessageType::WARNING, "Could not compute cache key for $packageName"); - } + yield from $this->client->window->logMessage(MessageType::WARNING, "Could not compute cache key for $packageName"); } + echo PHP_EOL; } + } - $duration = (int)(microtime(true) - $startTime); - $mem = (int)(memory_get_usage(true) / (1024 * 1024)); - $this->client->window->logMessage( - MessageType::INFO, - "All $count PHP files parsed in $duration seconds. $mem MiB allocated." - ); - }); + $duration = (int)(microtime(true) - $startTime); + $mem = (int)(memory_get_usage(true) / (1024 * 1024)); + yield from $this->client->window->logMessage( + MessageType::INFO, + "All $count PHP files parsed in $duration seconds. $mem MiB allocated." + ); } /** * @param array $files * @return Promise */ - private function indexFiles(array $files): Promise + private function indexFiles(array $files): \Generator { - return coroutine(function () use ($files) { - foreach ($files as $i => $uri) { - // Skip open documents - if ($this->documentLoader->isOpen($uri)) { - continue; - } - - // Give LS to the chance to handle requests while indexing - yield timeout(); - $this->client->window->logMessage(MessageType::LOG, "Parsing $uri"); - try { - $document = yield $this->documentLoader->load($uri); - if (!isVendored($document, $this->composerJson)) { - $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); - } - } catch (ContentTooLargeException $e) { - $this->client->window->logMessage( - MessageType::INFO, - "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" - ); - } catch (\Exception $e) { - $this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e); - } + foreach ($files as $i => $uri) { + // Skip open documents + if ($this->documentLoader->isOpen($uri)) { + continue; } - }); + + // Give LS to the chance to handle requests while indexing + yield new Delayed(0); + yield from $this->client->window->logMessage(MessageType::LOG, "Parsing $uri"); + try { + $document = yield from $this->documentLoader->load($uri); + if (!isVendored($document, $this->composerJson)) { + yield from $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + } + } catch (ContentTooLargeException $e) { + yield from $this->client->window->logMessage( + MessageType::INFO, + "Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})" + ); + } catch (\Exception $e) { + yield from $this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e); + } + } } } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 38dfeb1..6c4ca8d 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -1,24 +1,24 @@ shutdownDeferred = new Deferred(); + $this->protocolReader = $reader; - $this->protocolReader->on('close', function () { + $this->protocolReader->addListener('close', function () { $this->shutdown(); - $this->exit(); }); - $this->protocolReader->on('message', function (Message $msg) { - coroutine(function () use ($msg) { + $this->protocolWriter = $writer; + $this->client = new LanguageClient($reader, $writer); + $this->protocolReader->addListener('message', function (MessageEvent $messageEvent) use ($reader, $writer) { + $msg = $messageEvent->getMessage(); + Loop::defer(function () use ($msg) { // Ignore responses, this is the handler for requests and notifications if (AdvancedJsonRpc\Response::isResponse($msg->body)) { return; @@ -149,12 +154,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } else { $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); } - $this->protocolWriter->write(new Message($responseBody)); + yield from $this->protocolWriter->write(new Message($responseBody)); } - })->otherwise('\\LanguageServer\\crash'); + }); }); - $this->protocolWriter = $writer; - $this->client = new LanguageClient($reader, $writer); + } + + public function getshutdownDeferred(): Promise + { + return $this->shutdownDeferred->promise(); } /** @@ -163,15 +171,13 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process. + * @param string|null $rootUri * @return Promise */ public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise { - if ($rootPath === null && $rootUri !== null) { - $rootPath = uriToPath($rootUri); - } - return coroutine(function () use ($capabilities, $rootPath, $processId) { - + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $capabilities, $rootPath, $processId, $rootUri) { if ($capabilities->xfilesProvider) { $this->filesFinder = new ClientFilesFinder($this->client); } else { @@ -200,25 +206,25 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher ); if ($rootPath !== null) { - yield $this->beforeIndex($rootPath); + yield from $this->beforeIndex($rootPath); // Find composer.json if ($this->composerJson === null) { - $composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath)); + $composerJsonFiles = yield from $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath)); sortUrisLevelOrder($composerJsonFiles); if (!empty($composerJsonFiles)) { - $this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0])); + $this->composerJson = json_decode(yield from $this->contentRetriever->retrieve($composerJsonFiles[0])); } } // Find composer.lock if ($this->composerLock === null) { - $composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath)); + $composerLockFiles = yield from $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath)); sortUrisLevelOrder($composerLockFiles); if (!empty($composerLockFiles)) { - $this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0])); + $this->composerLock = json_decode(yield from $this->contentRetriever->retrieve($composerLockFiles[0])); } } @@ -236,7 +242,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->composerLock, $this->composerJson ); - $indexer->index()->otherwise('\\LanguageServer\\crash'); + Loop::defer(function () use ($indexer) { + yield from $indexer->index(); + }); } @@ -288,8 +296,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $serverCapabilities->xdefinitionProvider = true; $serverCapabilities->xdependenciesProvider = true; - return new InitializeResult($serverCapabilities); + $deferred->resolve(new InitializeResult($serverCapabilities)); }); + return $deferred->promise(); } /** @@ -297,29 +306,23 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher * (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 + * @return \Generator */ public function shutdown() { unset($this->project); - } - - /** - * A notification to ask the server to exit its process. - * - * @return void - */ - public function exit() - { - exit(0); + $this->shutdownDeferred->resolve(); + yield new Delayed(0); } /** * Called before indexing, can return a Promise * * @param string $rootPath + * @return \Generator */ protected function beforeIndex(string $rootPath) { + yield new Delayed(0, null); } } diff --git a/src/Message.php b/src/Message.php index fb51110..051182a 100644 --- a/src/Message.php +++ b/src/Message.php @@ -1,10 +1,9 @@ body = MessageBody::parse(array_pop($parts)); foreach ($parts as $line) { if ($line) { @@ -55,11 +54,11 @@ class Message { $body = (string)$this->body; $contentLength = strlen($body); - $this->headers['Content-Length'] = $contentLength; + $this->headers['Content-Length'] = $contentLength + 6; $headers = ''; foreach ($this->headers as $name => $value) { $headers .= "$name: $value\r\n"; } - return $headers . "\r\n" . $body; + return $headers . "\r\n" . $body . "\r\n\r\n\r\n"; } } diff --git a/src/PhpDocument.php b/src/PhpDocument.php index d148f9c..28ddbd8 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -1,5 +1,5 @@ PhpDocument of open documents that should be kept in memory * - * @var PhpDocument + * @var PhpDocument[] */ private $documents = []; @@ -37,11 +36,6 @@ class PhpDocumentLoader */ private $parser; - /** - * @var PhpParser\Parser - */ - private $tolerantParser; - /** * @var DocBlockFactory */ @@ -87,11 +81,16 @@ class PhpDocumentLoader * If the document is not open, loads it. * * @param string $uri - * @return Promise + * @return \Generator + * @throws ContentTooLargeException */ - public function getOrLoad(string $uri): Promise + public function getOrLoad(string $uri): \Generator { - return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($uri); + if (isset($this->documents[$uri])) { + return $this->documents[$uri]; + } else { + return yield from $this->load($uri); + } } /** @@ -100,27 +99,25 @@ class PhpDocumentLoader * The document is NOT added to the list of open documents, but definitions are registered. * * @param string $uri - * @return Promise + * @return \Generator + * @throws ContentTooLargeException */ - public function load(string $uri): Promise + public function load(string $uri): \Generator { - return coroutine(function () use ($uri) { + $limit = 150000; + $content = yield from $this->contentRetriever->retrieve($uri); + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } - $limit = 150000; - $content = yield $this->contentRetriever->retrieve($uri); - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = $this->create($uri, $content); - } - return $document; - }); + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = $this->create($uri, $content); + } + return $document; } /** @@ -147,7 +144,7 @@ class PhpDocumentLoader * * @param string $uri * @param string $content - * @return void + * @return PhpDocument */ public function open(string $uri, string $content) { diff --git a/src/ProtocolReader.php b/src/ProtocolReader.php index c199fba..d9a0d4a 100644 --- a/src/ProtocolReader.php +++ b/src/ProtocolReader.php @@ -1,9 +1,9 @@ input = $input; - - $this->on('close', function () { - Loop\removeReadStream($this->input); - }); - - Loop\addReadStream($this->input, function () { - if (feof($this->input)) { - // If stream_select reported a status change for this stream, - // but the stream is EOF, it means it was closed. - $this->emit('close'); - return; - } - while (($c = fgetc($this->input)) !== false && $c !== '') { - $this->buffer .= $c; - switch ($this->parsingMode) { - case self::PARSE_HEADERS: - if ($this->buffer === "\r\n") { - $this->parsingMode = self::PARSE_BODY; - $this->contentLength = (int)$this->headers['Content-Length']; - $this->buffer = ''; - } else if (substr($this->buffer, -2) === "\r\n") { - $parts = explode(':', $this->buffer); - $this->headers[$parts[0]] = trim($parts[1]); - $this->buffer = ''; - } - break; - case self::PARSE_BODY: - if (strlen($this->buffer) === $this->contentLength) { - $msg = new Message(MessageBody::parse($this->buffer), $this->headers); - $this->emit('message', [$msg]); - $this->parsingMode = self::PARSE_HEADERS; - $this->headers = []; - $this->buffer = ''; + Loop::defer(function () use (&$input) { + $buffer = ''; + while (true) { + $headers = []; + while (true) { + while (($pos = strpos($buffer, "\r\n")) === false) { + $read = yield $input->read(); + if ($read === null) { + return; } + $buffer .= $read; + } + + $headerLine = substr($buffer, 0, $pos); + $buffer = substr($buffer, (int)$pos + 2); + if (!$headerLine) { break; + } + $headerPairs = \explode(': ', $headerLine); + $headers[$headerPairs[0]] = $headerPairs[1]; } + $contentLength = (int)$headers['Content-Length']; + while (strlen($buffer) < $contentLength) { + $read = yield $this->input->read(); + if ($read === null) { + return; + } + $buffer .= $read; + } + $body = substr($buffer, 0, $contentLength); + $buffer = substr($buffer, $contentLength); + $msg = new Message(MessageBody::parse($body), $headers); + $this->emit(new MessageEvent('message', $msg)); } }); } diff --git a/src/ProtocolStreamWriter.php b/src/ProtocolStreamWriter.php index 166ea90..e4712cc 100644 --- a/src/ProtocolStreamWriter.php +++ b/src/ProtocolStreamWriter.php @@ -1,8 +1,9 @@ output = $output; } @@ -32,21 +33,9 @@ class ProtocolStreamWriter implements ProtocolWriter /** * {@inheritdoc} */ - public function write(Message $msg): Promise + public function write(Message $msg): \Generator { - // if the message queue is currently empty, register a write handler. - if (empty($this->messages)) { - Loop\addWriteStream($this->output, function () { - $this->flush(); - }); - } - - $promise = new Promise(); - $this->messages[] = [ - 'message' => (string)$msg, - 'promise' => $promise - ]; - return $promise; + yield $this->output->write((string)$msg); } /** diff --git a/src/ProtocolWriter.php b/src/ProtocolWriter.php index ac25218..005f412 100644 --- a/src/ProtocolWriter.php +++ b/src/ProtocolWriter.php @@ -14,5 +14,5 @@ interface ProtocolWriter * @param Message $msg * @return Promise Resolved when the message has been fully written out to the output stream */ - public function write(Message $msg): Promise; + public function write(Message $msg): \Generator; } diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 039ff57..a7ac5f3 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -1,37 +1,35 @@ documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) { + $deferred = new Deferred(); + Loop::defer(function () use ($textDocument, $deferred) { + /** @var PhpDocument $document */ + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); + $symbols = []; foreach ($document->getDefinitions() as $fqn => $definition) { $symbols[] = $definition->symbolInformation; } - return $symbols; + $deferred->resolve($symbols); }); + return $deferred->promise(); } /** @@ -134,10 +137,12 @@ class TextDocument */ public function didOpen(TextDocumentItem $textDocument) { - $document = $this->documentLoader->open($textDocument->uri, $textDocument->text); - if (!isVendored($document, $this->composerJson)) { - $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); - } + Loop::defer(function () use ($textDocument) { + $document = $this->documentLoader->open($textDocument->uri, $textDocument->text); + if (!isVendored($document, $this->composerJson)) { + yield from $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); + } + }); } /** @@ -145,13 +150,18 @@ class TextDocument * * @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument * @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges - * @return void + * @return Promise */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $document = $this->documentLoader->get($textDocument->uri); - $document->updateContent($contentChanges[0]->text); - $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $contentChanges) { + $document = $this->documentLoader->get($textDocument->uri); + $document->updateContent($contentChanges[0]->text); + yield from $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); + $deferred->resolve(); + }); + return $deferred->promise(); } /** @@ -179,8 +189,9 @@ class TextDocument TextDocumentIdentifier $textDocument, Position $position ): Promise { - return coroutine(function () use ($textDocument, $position) { - $document = yield $this->documentLoader->getOrLoad($textDocument->uri); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $position) { + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; @@ -189,8 +200,7 @@ class TextDocument // Variables always stay in the boundary of the file and need to be searched inside their function scope // by traversing the AST if ( - - ($node instanceof Node\Expression\Variable && !($node->getParent()->getParent() instanceof Node\PropertyDeclaration)) + ($node instanceof Node\Expression\Variable && !($node->getParent()->getParent() instanceof Node\PropertyDeclaration)) || $node instanceof Node\Parameter || $node instanceof Node\UseVariableName ) { @@ -217,21 +227,18 @@ class TextDocument // Definition with a global FQN $fqn = DefinitionResolver::getDefinedFqn($node); - // Wait until indexing finished - if (!$this->index->isComplete()) { - yield waitForEvent($this->index, 'complete'); - } if ($fqn === null) { $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node); if ($fqn === null) { - return []; + $deferred->resolve([]); + return; } } $refDocumentPromises = []; foreach ($this->index->getReferenceUris($fqn) as $uri) { - $refDocumentPromises[] = $this->documentLoader->getOrLoad($uri); + $refDocumentPromises[] = new Coroutine($this->documentLoader->getOrLoad($uri)); } - $refDocuments = yield Promise\all($refDocumentPromises); + $refDocuments = yield \Amp\Promise\all($refDocumentPromises); foreach ($refDocuments as $document) { $refs = $document->getReferenceNodesByFqn($fqn); if ($refs !== null) { @@ -241,8 +248,9 @@ class TextDocument } } } - return $locations; + $deferred->resolve($locations); }); + return $deferred->promise(); } /** @@ -250,16 +258,20 @@ class TextDocument * cursor position. * * @param TextDocumentIdentifier $textDocument The text document - * @param Position $position The position inside the text document + * @param Position $position The position inside the text document * * @return Promise */ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise { - return coroutine(function () use ($textDocument, $position) { - $document = yield $this->documentLoader->getOrLoad($textDocument->uri); - return $this->signatureHelpProvider->getSignatureHelp($document, $position); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $position) { + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); + $deferred->resolve( + yield from $this->signatureHelpProvider->getSignatureHelp($document, $position) + ); }); + return $deferred->promise(); } /** @@ -269,11 +281,13 @@ class TextDocument * @param TextDocumentIdentifier $textDocument The text document * @param Position $position The position inside the text document * @return Promise + * @throws \LanguageServer\ContentTooLargeException */ public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { - return coroutine(function () use ($textDocument, $position) { - $document = yield $this->documentLoader->getOrLoad($textDocument->uri); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $position) { + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; @@ -291,17 +305,18 @@ class TextDocument if ($def !== null || $this->index->isComplete()) { break; } - yield waitForEvent($this->index, 'definition-added'); } if ( $def === null || $def->symbolInformation === null - || Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' + || parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' ) { - return []; + $deferred->resolve([]); + } else { + $deferred->resolve($def->symbolInformation->location); } - return $def->symbolInformation->location; }); + return $deferred->promise(); } /** @@ -313,8 +328,9 @@ class TextDocument */ public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { - return coroutine(function () use ($textDocument, $position) { - $document = yield $this->documentLoader->getOrLoad($textDocument->uri); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $position) { + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); // Find the node under the cursor $node = $document->getNodeAtPosition($position); if ($node === null) { @@ -333,7 +349,6 @@ class TextDocument if ($def !== null || $this->index->isComplete()) { break; } - yield waitForEvent($this->index, 'definition-added'); } $range = RangeFactory::fromNode($node); if ($def === null) { @@ -346,8 +361,9 @@ class TextDocument if ($def->documentation) { $contents[] = $def->documentation; } - return new Hover($contents, $range); + $deferred->resolve(new Hover($contents, $range)); }); + return $deferred->promise(); } /** @@ -360,17 +376,20 @@ class TextDocument * interface then a 'completionItem/resolve' request is sent with the selected completion item as a param. The * returned completion item should have the documentation property filled in. * - * @param TextDocumentIdentifier The text document + * @param TextDocumentIdentifier $textDocument * @param Position $position The position * @param CompletionContext|null $context The completion context * @return Promise */ public function completion(TextDocumentIdentifier $textDocument, Position $position, CompletionContext $context = null): Promise { - return coroutine(function () use ($textDocument, $position, $context) { - $document = yield $this->documentLoader->getOrLoad($textDocument->uri); - return $this->completionProvider->provideCompletion($document, $position, $context); + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $context, $position, $textDocument) { + /** @var PhpDocument $document */ + $document = yield from $this->documentLoader->getOrLoad($textDocument->uri); + $deferred->resolve($this->completionProvider->provideCompletion($document, $position, $context)); }); + return $deferred->promise(); } /** @@ -382,12 +401,13 @@ class TextDocument * but still may know some information about it. * * @param TextDocumentIdentifier $textDocument The text document - * @param Position $position The position inside the text document + * @param Position $position The position inside the text document * @return Promise */ public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): Promise { - return coroutine(function () use ($textDocument, $position) { + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $textDocument, $position) { $document = yield $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { @@ -406,12 +426,11 @@ class TextDocument if ($def !== null || $this->index->isComplete()) { break; } - yield waitForEvent($this->index, 'definition-added'); } if ( $def === null || $def->symbolInformation === null - || Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' + || parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' ) { return []; } @@ -422,7 +441,8 @@ class TextDocument $packageName = $this->composerJson->name; } $descriptor = new SymbolDescriptor($def->fqn, new PackageDescriptor($packageName)); - return [new SymbolLocationInformation($descriptor, $def->symbolInformation->location)]; + $deferred->resolve([new SymbolLocationInformation($descriptor, $def->symbolInformation->location)]); }); + return $deferred->promise(); } } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index fd93a9e..e86d83a 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -1,23 +1,17 @@ sourceIndex->isStaticComplete()) { - yield waitForEvent($this->sourceIndex, 'static-complete'); + $symbols = []; + foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) { + if ($query === '' || stripos($fqn, $query) !== false) { + $symbols[] = $definition->symbolInformation; } - $symbols = []; - foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) { - if ($query === '' || stripos($fqn, $query) !== false) { - $symbols[] = $definition->symbolInformation; - } - } - return $symbols; - }); + } + return new Success($symbols); } /** @@ -106,35 +94,35 @@ class Workspace */ public function didChangeWatchedFiles(array $changes) { - foreach ($changes as $change) { - if ($change->type === FileChangeType::DELETED) { - $this->client->textDocument->publishDiagnostics($change->uri, []); + Loop::defer(function () use ($changes) { + foreach ($changes as $change) { + if ($change->type === FileChangeType::DELETED) { + yield from $this->client->textDocument->publishDiagnostics($change->uri, []); + } } - } + }); } /** * 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. - * @return ReferenceInformation[] + * @param string[] $files An optional list of files to restrict the search to. + * @return \Generator + * @throws \LanguageServer\ContentTooLargeException */ - public function xreferences($query, array $files = null): Promise + public function xreferences($query, array $files = null): \Generator { - // TODO: $files is unused in the coroutine - return coroutine(function () use ($query, $files) { + $deferred = new Deferred(); + Loop::defer(function () use ($deferred, $query, $files) { + // TODO: $files is unused in the coroutine if ($this->composerLock === null) { return []; } - // Wait until indexing finished - if (!$this->projectIndex->isComplete()) { - yield waitForEvent($this->projectIndex, 'complete'); - } /** Map from URI to array of referenced FQNs in dependencies */ $refs = []; // Get all references TO dependencies - $fqns = isset($query->fqsen) ? [$query->fqsen] : array_values($this->dependenciesIndex->getDefinitions()); + $fqns = isset($query->fqsen) ? [$query->fqsen] : array_values(yield from $this->dependenciesIndex->getDefinitions()); foreach ($fqns as $fqn) { foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) { if (!isset($refs[$uri])) { @@ -148,7 +136,7 @@ class Workspace $refInfos = []; foreach ($refs as $uri => $fqns) { foreach ($fqns as $fqn) { - $doc = yield $this->documentLoader->getOrLoad($uri); + $doc = yield from $this->documentLoader->getOrLoad($uri); foreach ($doc->getReferenceNodesByFqn($fqn) as $node) { $refInfo = new ReferenceInformation; $refInfo->reference = LocationFactory::fromNode($node); @@ -157,8 +145,9 @@ class Workspace } } } - return $refInfos; + $deferred->resolve($refInfos); }); + return $deferred->promise(); } /** diff --git a/src/SignatureHelpProvider.php b/src/SignatureHelpProvider.php index 66afd5f..fb2601b 100644 --- a/src/SignatureHelpProvider.php +++ b/src/SignatureHelpProvider.php @@ -1,16 +1,13 @@ */ - public function getSignatureHelp(PhpDocument $doc, Position $position): Promise + public function getSignatureHelp(PhpDocument $doc, Position $position): \Generator { - return coroutine(function () use ($doc, $position) { - // Find the node under the cursor - $node = $doc->getNodeAtPosition($position); + // Find the node under the cursor + $node = $doc->getNodeAtPosition($position); - // Find the definition of the item being called - list($def, $argumentExpressionList) = yield $this->getCallingInfo($node); + // Find the definition of the item being called + list($def, $argumentExpressionList) = yield from $this->getCallingInfo($node); - if (!$def || !$def->signatureInformation) { - return new SignatureHelp(); - } + if (!$def || !$def->signatureInformation) { + return new SignatureHelp(); + } - // Find the active parameter - $activeParam = $argumentExpressionList - ? $this->findActiveParameter($argumentExpressionList, $position, $doc) - : 0; + // Find the active parameter + $activeParam = $argumentExpressionList + ? $this->findActiveParameter($argumentExpressionList, $position, $doc) + : 0; - return new SignatureHelp([$def->signatureInformation], 0, $activeParam); - }); + return new SignatureHelp([$def->signatureInformation], 0, $activeParam); } /** @@ -73,83 +68,81 @@ class SignatureHelpProvider * * @param Node $node The node to find calling information from * - * @return Promise + * @return \Generator */ - private function getCallingInfo(Node $node) + private function getCallingInfo(Node $node): \Generator { - return coroutine(function () use ($node) { - $fqn = null; - $callingNode = null; - if ($node instanceof Node\DelimitedList\ArgumentExpressionList) { - // Cursor is already inside a ( - $argumentExpressionList = $node; - if ($node->parent instanceof Node\Expression\ObjectCreationExpression) { - // Constructing something - $callingNode = $node->parent->classTypeDesignator; - if (!$callingNode instanceof Node\QualifiedName) { - // We only support constructing from a QualifiedName - return null; - } - $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($callingNode); - $fqn = "{$fqn}->__construct()"; - } else { - $callingNode = $node->parent->getFirstChildNode( - Node\Expression\MemberAccessExpression::class, - Node\Expression\ScopedPropertyAccessExpression::class, - Node\QualifiedName::class - ); - } - } elseif ($node instanceof Node\Expression\CallExpression) { - $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); - $callingNode = $node->getFirstChildNode( - Node\Expression\MemberAccessExpression::class, - Node\Expression\ScopedPropertyAccessExpression::class, - Node\QualifiedName::class - ); - } elseif ($node instanceof Node\Expression\ObjectCreationExpression) { - $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); - $callingNode = $node->classTypeDesignator; + $fqn = null; + $callingNode = null; + if ($node instanceof Node\DelimitedList\ArgumentExpressionList) { + // Cursor is already inside a ( + $argumentExpressionList = $node; + if ($node->parent instanceof Node\Expression\ObjectCreationExpression) { + // Constructing something + $callingNode = $node->parent->classTypeDesignator; if (!$callingNode instanceof Node\QualifiedName) { // We only support constructing from a QualifiedName return null; } - // Manually build the __construct fqn $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($callingNode); $fqn = "{$fqn}->__construct()"; + } else { + $callingNode = $node->parent->getFirstChildNode( + Node\Expression\MemberAccessExpression::class, + Node\Expression\ScopedPropertyAccessExpression::class, + Node\QualifiedName::class + ); } - - if (!$callingNode) { + } elseif ($node instanceof Node\Expression\CallExpression) { + $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); + $callingNode = $node->getFirstChildNode( + Node\Expression\MemberAccessExpression::class, + Node\Expression\ScopedPropertyAccessExpression::class, + Node\QualifiedName::class + ); + } elseif ($node instanceof Node\Expression\ObjectCreationExpression) { + $argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class); + $callingNode = $node->classTypeDesignator; + if (!$callingNode instanceof Node\QualifiedName) { + // We only support constructing from a QualifiedName return null; } + // Manually build the __construct fqn + $fqn = $this->definitionResolver->resolveReferenceNodeToFqn($callingNode); + $fqn = "{$fqn}->__construct()"; + } - // Now find the definition of the call - $fqn = $fqn ?: DefinitionResolver::getDefinedFqn($callingNode); - while (true) { - if ($fqn) { - $def = $this->index->getDefinition($fqn); - } else { - $def = $this->definitionResolver->resolveReferenceNodeToDefinition($callingNode); - } - if ($def !== null || $this->index->isComplete()) { - break; - } - yield waitForEvent($this->index, 'definition-added'); + if (!$callingNode) { + return null; + } + + // Now find the definition of the call + $fqn = $fqn ?: DefinitionResolver::getDefinedFqn($callingNode); + while (true) { + if ($fqn) { + $def = $this->index->getDefinition($fqn); + } else { + $def = $this->definitionResolver->resolveReferenceNodeToDefinition($callingNode); } - - if (!$def) { - return null; + if ($def !== null || $this->index->isComplete()) { + break; } + } - return [$def, $argumentExpressionList]; - }); + if (!$def) { + return null; + } + + yield new Delayed(0); + return [$def, $argumentExpressionList]; } /** * Given a position and arguments, finds the "active" argument at the position * * @param Node\DelimitedList\ArgumentExpressionList $argumentExpressionList The argument expression list - * @param Position $position The position to detect the active argument from - * @param PhpDocument $doc The document that contains the expression + * @param Position $position The position to detect the active argument from + * @param PhpDocument $doc The document that contains the expression * * @return int */ diff --git a/src/utils.php b/src/utils.php index af47a96..50493c1 100644 --- a/src/utils.php +++ b/src/utils.php @@ -1,12 +1,12 @@ - */ -function timeout($seconds = 0): Promise -{ - $promise = new Promise; - Loop\setTimeout([$promise, 'fulfill'], $seconds); - return $promise; -} - -/** - * Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter - * - * @param EmitterInterface $emitter - * @param string $event - * @return Promise - */ -function waitForEvent(EmitterInterface $emitter, string $event): Promise -{ - $p = new Promise; - $emitter->once($event, [$p, 'fulfill']); - return $p; -} - /** * Returns the part of $b that is not overlapped by $a * Example: @@ -125,7 +97,7 @@ function stripStringOverlap(string $a, string $b): string function sortUrisLevelOrder(&$uriList) { usort($uriList, function ($a, $b) { - return substr_count(Uri\parse($a)['path'], '/') - substr_count(Uri\parse($b)['path'], '/'); + return substr_count(parse($a)['path'], '/') - substr_count(parse($b)['path'], '/'); }); } @@ -133,13 +105,13 @@ function sortUrisLevelOrder(&$uriList) * Checks a document against the composer.json to see if it * is a vendored document * - * @param PhpDocument $document + * @param PhpDocument $document * @param \stdClass|null $composerJson * @return bool */ function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool { - $path = Uri\parse($document->getUri())['path']; + $path = parse($document->getUri())['path']; $vendorDir = getVendorDir($composerJson); return strpos($path, "/$vendorDir/") !== false; } @@ -148,7 +120,7 @@ function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool * Check a given URI against the composer.json to see if it * is a vendored URI * - * @param string $uri + * @param string $uri * @param \stdClass|null $composerJson * @return string|null */