move to amphp
parent
bc07c19957
commit
9a65f2a872
|
@ -1,8 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
|
use Amp\ByteStream\ResourceInputStream;
|
||||||
use Sabre\Event\Loop;
|
use Amp\ByteStream\ResourceOutputStream;
|
||||||
|
use Amp\Loop;
|
||||||
|
use Amp\Socket\ClientSocket;
|
||||||
|
use Amp\Socket\ServerSocket;
|
||||||
use Composer\XdebugHandler\XdebugHandler;
|
use Composer\XdebugHandler\XdebugHandler;
|
||||||
|
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
|
||||||
|
|
||||||
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
|
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
|
||||||
|
|
||||||
|
@ -42,69 +46,51 @@ unset($xdebugHandler);
|
||||||
if (!empty($options['tcp'])) {
|
if (!empty($options['tcp'])) {
|
||||||
// Connect to a TCP server
|
// Connect to a TCP server
|
||||||
$address = $options['tcp'];
|
$address = $options['tcp'];
|
||||||
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
|
$server = function () use ($logger, $address) {
|
||||||
if ($socket === false) {
|
/** @var ClientSocket $socket */
|
||||||
$logger->critical("Could not connect to language client. Error $errno\n$errstr");
|
$socket = yield Amp\Socket\connect('tcp://' . $address);
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
stream_set_blocking($socket, false);
|
|
||||||
$ls = new LanguageServer(
|
$ls = new LanguageServer(
|
||||||
new ProtocolStreamReader($socket),
|
new ProtocolStreamReader($socket),
|
||||||
new ProtocolStreamWriter($socket)
|
new ProtocolStreamWriter($socket)
|
||||||
);
|
);
|
||||||
Loop\run();
|
yield $ls->getshutdownDeferred();
|
||||||
|
};
|
||||||
} else if (!empty($options['tcp-server'])) {
|
} else if (!empty($options['tcp-server'])) {
|
||||||
// Run a TCP Server
|
// Run a TCP Server
|
||||||
$address = $options['tcp-server'];
|
$address = $options['tcp-server'];
|
||||||
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
$server = function () use ($logger, $address) {
|
||||||
if ($tcpServer === false) {
|
|
||||||
$logger->critical("Could not listen on $address. Error $errno\n$errstr");
|
$server = Amp\Socket\listen('tcp://' . $address);
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
$logger->debug("Server listening on $address");
|
$logger->debug("Server listening on $address");
|
||||||
$pcntlAvailable = extension_loaded('pcntl');
|
|
||||||
if (!$pcntlAvailable) {
|
while ($socket = yield $server->accept()) {
|
||||||
$logger->notice('PCNTL is not available. Only a single connection will be accepted');
|
/** @var ServerSocket $socket */
|
||||||
}
|
list($ip, $port) = \explode(':', $socket->getRemoteAddress());
|
||||||
while ($socket = stream_socket_accept($tcpServer, -1)) {
|
|
||||||
$logger->debug('Connection accepted');
|
$logger->debug("Accepted connection from {$ip}:{$port}." . PHP_EOL);
|
||||||
stream_set_blocking($socket, false);
|
|
||||||
if ($pcntlAvailable) {
|
Loop::run(function () use ($socket) {
|
||||||
// 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(
|
$ls = new LanguageServer(
|
||||||
new ProtocolStreamReader($socket),
|
new ProtocolStreamReader($socket),
|
||||||
new ProtocolStreamWriter($socket)
|
new ProtocolStreamWriter($socket)
|
||||||
);
|
);
|
||||||
Loop\run();
|
yield $ls->getshutdownDeferred();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Use STDIO
|
// Use STDIO
|
||||||
$logger->debug('Listening on STDIN');
|
$logger->debug('Listening on STDIN');
|
||||||
stream_set_blocking(STDIN, false);
|
$inputStream = new ResourceInputStream(STDIN);
|
||||||
|
$outputStream = new ResourceOutputStream(STDOUT);
|
||||||
$ls = new LanguageServer(
|
$ls = new LanguageServer(
|
||||||
new ProtocolStreamReader(STDIN),
|
new ProtocolStreamReader($inputStream),
|
||||||
new ProtocolStreamWriter(STDOUT)
|
new ProtocolStreamWriter($outputStream)
|
||||||
);
|
);
|
||||||
Loop\run();
|
$server = function () use ($ls) {
|
||||||
|
yield $ls->getshutdownDeferred();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loop::run($server);
|
||||||
|
|
|
@ -22,17 +22,21 @@
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.0",
|
"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",
|
"composer/xdebug-handler": "^1.0",
|
||||||
"felixfbecker/advanced-json-rpc": "^3.0.0",
|
"felixfbecker/advanced-json-rpc": "^3.0.0",
|
||||||
"felixfbecker/language-server-protocol": "^1.0.1",
|
"felixfbecker/language-server-protocol": "^1.0.1",
|
||||||
"jetbrains/phpstorm-stubs": "dev-master",
|
"jetbrains/phpstorm-stubs": "dev-master",
|
||||||
|
"league/event": "^2.2",
|
||||||
|
"league/uri-parser": "^1.4",
|
||||||
"microsoft/tolerant-php-parser": "0.0.*",
|
"microsoft/tolerant-php-parser": "0.0.*",
|
||||||
"netresearch/jsonmapper": "^1.0",
|
"netresearch/jsonmapper": "^1.0",
|
||||||
"php-ds/php-ds": "^1.2",
|
"php-ds/php-ds": "^1.2",
|
||||||
"phpdocumentor/reflection-docblock": "^4.0.0",
|
"phpdocumentor/reflection-docblock": "^4.0.0",
|
||||||
"psr/log": "^1.0",
|
"psr/log": "^1.0",
|
||||||
"sabre/event": "^5.0",
|
|
||||||
"sabre/uri": "^2.0",
|
|
||||||
"webmozart/glob": "^4.1",
|
"webmozart/glob": "^4.1",
|
||||||
"webmozart/path-util": "^2.3"
|
"webmozart/path-util": "^2.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,8 +3,6 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Cache;
|
namespace LanguageServer\Cache;
|
||||||
|
|
||||||
use Sabre\Event\Promise;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A key/value store for caching purposes
|
* A key/value store for caching purposes
|
||||||
*/
|
*/
|
||||||
|
@ -14,16 +12,16 @@ interface Cache
|
||||||
* Gets a value from the cache
|
* Gets a value from the cache
|
||||||
*
|
*
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return Promise <mixed>
|
* @return \Generator <mixed>
|
||||||
*/
|
*/
|
||||||
public function get(string $key): Promise;
|
public function get(string $key): \Generator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a value in the cache
|
* Sets a value in the cache
|
||||||
*
|
*
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return Promise
|
* @return \Generator
|
||||||
*/
|
*/
|
||||||
public function set(string $key, $value): Promise;
|
public function set(string $key, $value): \Generator;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,11 +30,11 @@ class ClientCache implements Cache
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return Promise <mixed>
|
* @return Promise <mixed>
|
||||||
*/
|
*/
|
||||||
public function get(string $key): Promise
|
public function get(string $key): \Generator
|
||||||
{
|
{
|
||||||
return $this->client->xcache->get($key)->then('unserialize')->otherwise(function () {
|
$cached = yield from $this->client->xcache->get($key);
|
||||||
// Ignore
|
$obj = unserialize($cached);
|
||||||
});
|
return $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,10 +44,8 @@ class ClientCache implements Cache
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return Promise
|
* @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 () {
|
return yield from $this->client->xcache->set($key, serialize($value));
|
||||||
// Ignore
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Cache;
|
namespace LanguageServer\Cache;
|
||||||
|
|
||||||
use Sabre\Event\Promise;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches content on the file system
|
* Caches content on the file system
|
||||||
*/
|
*/
|
||||||
|
@ -30,18 +28,16 @@ class FileSystemCache implements Cache
|
||||||
* Gets a value from the cache
|
* Gets a value from the cache
|
||||||
*
|
*
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return Promise <mixed>
|
* @return \Generator <mixed>
|
||||||
*/
|
*/
|
||||||
public function get(string $key): Promise
|
public function get(string $key): \Generator
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$file = $this->cacheDir . urlencode($key);
|
$file = $this->cacheDir . urlencode($key);
|
||||||
if (!file_exists($file)) {
|
$content = yield \Amp\File\get($file);
|
||||||
return Promise\resolve(null);
|
return unserialize($content);
|
||||||
}
|
|
||||||
return Promise\resolve(unserialize(file_get_contents($file)));
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return Promise\resolve(null);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,18 +46,18 @@ class FileSystemCache implements Cache
|
||||||
*
|
*
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return Promise
|
* @return \Generator
|
||||||
*/
|
*/
|
||||||
public function set(string $key, $value): Promise
|
public function set(string $key, $value): \Generator
|
||||||
{
|
{
|
||||||
try {
|
|
||||||
$file = $this->cacheDir . urlencode($key);
|
$file = $this->cacheDir . urlencode($key);
|
||||||
if (!file_exists($this->cacheDir)) {
|
$dir = dirname($file);
|
||||||
mkdir($this->cacheDir);
|
if (yield \Amp\File\isfile($dir)) {
|
||||||
}
|
yield \Amp\File\unlink($dir);
|
||||||
file_put_contents($file, serialize($value));
|
|
||||||
} finally {
|
|
||||||
return Promise\resolve(null);
|
|
||||||
}
|
}
|
||||||
|
if (!yield \Amp\File\exists($dir)) {
|
||||||
|
yield \Amp\File\mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
yield \Amp\File\put($file, serialize($value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,9 +36,9 @@ class TextDocument
|
||||||
* @param Diagnostic[] $diagnostics
|
* @param Diagnostic[] $diagnostics
|
||||||
* @return Promise <void>
|
* @return Promise <void>
|
||||||
*/
|
*/
|
||||||
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,
|
'uri' => $uri,
|
||||||
'diagnostics' => $diagnostics
|
'diagnostics' => $diagnostics
|
||||||
]);
|
]);
|
||||||
|
@ -51,13 +51,12 @@ class TextDocument
|
||||||
* @param TextDocumentIdentifier $textDocument The document to get the content for
|
* @param TextDocumentIdentifier $textDocument The document to get the content for
|
||||||
* @return Promise <TextDocumentItem> The document's current content
|
* @return Promise <TextDocumentItem> 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/xcontent',
|
||||||
['textDocument' => $textDocument]
|
['textDocument' => $textDocument]
|
||||||
)->then(function ($result) {
|
);
|
||||||
return $this->mapper->map($result, new TextDocumentItem);
|
return $this->mapper->map($result, new TextDocumentItem);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ class Window
|
||||||
* @param string $message
|
* @param string $message
|
||||||
* @return Promise <void>
|
* @return Promise <void>
|
||||||
*/
|
*/
|
||||||
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,13 +35,12 @@ class Workspace
|
||||||
* @param string $base The base directory (defaults to the workspace)
|
* @param string $base The base directory (defaults to the workspace)
|
||||||
* @return Promise <TextDocumentIdentifier[]> Array of documents
|
* @return Promise <TextDocumentIdentifier[]> 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',
|
'workspace/xfiles',
|
||||||
['base' => $base]
|
['base' => $base]
|
||||||
)->then(function (array $textDocuments) {
|
);
|
||||||
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
|
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,9 @@ class XCache
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return Promise <mixed>
|
* @return Promise <mixed>
|
||||||
*/
|
*/
|
||||||
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,8 +35,8 @@ class XCache
|
||||||
* @param mixed $value
|
* @param mixed $value
|
||||||
* @return Promise <mixed>
|
* @return Promise <mixed>
|
||||||
*/
|
*/
|
||||||
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use AdvancedJsonRpc;
|
use AdvancedJsonRpc;
|
||||||
use Sabre\Event\Promise;
|
use Amp\Deferred;
|
||||||
|
use Amp\Loop;
|
||||||
|
use LanguageServer\Event\MessageEvent;
|
||||||
|
|
||||||
class ClientHandler
|
class ClientHandler
|
||||||
{
|
{
|
||||||
|
@ -35,31 +37,35 @@ class ClientHandler
|
||||||
*
|
*
|
||||||
* @param string $method The method to call
|
* @param string $method The method to call
|
||||||
* @param array|object $params The method parameters
|
* @param array|object $params The method parameters
|
||||||
* @return Promise <mixed> Resolved with the result of the request or rejected with an error
|
* @return \Generator <mixed> 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();
|
$id = $this->idGenerator->generate();
|
||||||
return $this->protocolWriter->write(
|
$deferred = new Deferred();
|
||||||
new Message(
|
$listener = function (MessageEvent $messageEvent) use ($id, $deferred, &$listener) {
|
||||||
new AdvancedJsonRpc\Request($id, $method, (object)$params)
|
$msg = $messageEvent->getMessage();
|
||||||
)
|
Loop::defer(function () use (&$listener, $deferred, $id, $msg) {
|
||||||
)->then(function () use ($id) {
|
|
||||||
$promise = new Promise;
|
|
||||||
$listener = function (Message $msg) use ($id, $promise, &$listener) {
|
|
||||||
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
|
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
|
||||||
// Received a response
|
// Received a response
|
||||||
$this->protocolReader->removeListener('message', $listener);
|
$this->protocolReader->removeListener('message', $listener);
|
||||||
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
|
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
|
||||||
$promise->fulfill($msg->body->result);
|
$deferred->resolve($msg->body->result);
|
||||||
} else {
|
} 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 string $method The method to call
|
||||||
* @param array|object $params The method parameters
|
* @param array|object $params The method parameters
|
||||||
* @return Promise <null> Will be resolved as soon as the notification has been sent
|
* @return \Generator <null> 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 Message(
|
||||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,14 +3,14 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\FilesFinder\FileSystemFilesFinder;
|
use Amp\Loop;
|
||||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||||
|
use LanguageServer\FilesFinder\FileSystemFilesFinder;
|
||||||
use LanguageServer\Index\StubsIndex;
|
use LanguageServer\Index\StubsIndex;
|
||||||
|
use Microsoft\PhpParser;
|
||||||
use phpDocumentor\Reflection\DocBlockFactory;
|
use phpDocumentor\Reflection\DocBlockFactory;
|
||||||
use Webmozart\PathUtil\Path;
|
use Webmozart\PathUtil\Path;
|
||||||
use Sabre\Uri;
|
use function League\Uri\parse;
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
use Microsoft\PhpParser;
|
|
||||||
|
|
||||||
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
|
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
|
@ -23,8 +23,7 @@ class ComposerScripts
|
||||||
{
|
{
|
||||||
public static function parseStubs()
|
public static function parseStubs()
|
||||||
{
|
{
|
||||||
coroutine(function () {
|
Loop::run(function () {
|
||||||
|
|
||||||
$index = new StubsIndex;
|
$index = new StubsIndex;
|
||||||
|
|
||||||
$finder = new FileSystemFilesFinder;
|
$finder = new FileSystemFilesFinder;
|
||||||
|
@ -44,20 +43,19 @@ class ComposerScripts
|
||||||
throw new \Exception('jetbrains/phpstorm-stubs package not found');
|
throw new \Exception('jetbrains/phpstorm-stubs package not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$uris = yield $finder->find("$stubsLocation/**/*.php");
|
$uris = yield from $finder->find("$stubsLocation/**/*.php");
|
||||||
|
|
||||||
foreach ($uris as $uri) {
|
foreach ($uris as $uri) {
|
||||||
echo "Parsing $uri\n";
|
echo "Parsing $uri\n";
|
||||||
$content = yield $contentRetriever->retrieve($uri);
|
$content = yield from $contentRetriever->retrieve($uri);
|
||||||
|
|
||||||
// Change URI to phpstubs://
|
// Change URI to phpstubs://
|
||||||
$parts = Uri\parse($uri);
|
$parts = parse($uri);
|
||||||
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
|
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
|
||||||
$parts['scheme'] = 'phpstubs';
|
$parts['scheme'] = 'phpstubs';
|
||||||
$uri = Uri\build($parts);
|
|
||||||
|
|
||||||
// Create a new document and add it to $index
|
// 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();
|
$index->setComplete();
|
||||||
|
@ -67,6 +65,6 @@ class ComposerScripts
|
||||||
$index->save();
|
$index->save();
|
||||||
|
|
||||||
echo "Finished\n";
|
echo "Finished\n";
|
||||||
})->wait();
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,10 @@ class ClientContentRetriever implements ContentRetriever
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
* @return Promise <string> Resolved with the content as a string
|
* @return Promise <string> 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))
|
/** @var TextDocumentItem $textDocument */
|
||||||
->then(function (TextDocumentItem $textDocument) {
|
$textDocument = yield from $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri));
|
||||||
return $textDocument->text;
|
return $textDocument->text;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,5 @@ interface ContentRetriever
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
* @return Promise <string> Resolved with the content as a string
|
* @return Promise <string> Resolved with the content as a string
|
||||||
*/
|
*/
|
||||||
public function retrieve(string $uri): Promise;
|
public function retrieve(string $uri): \Generator;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ class FileSystemContentRetriever implements ContentRetriever
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
* @return Promise <string> Resolved with the content as a string
|
* @return Promise <string> 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Event;
|
||||||
|
|
||||||
|
use LanguageServer\Message;
|
||||||
|
use League\Event\Event;
|
||||||
|
|
||||||
|
class MessageEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Message
|
||||||
|
*/
|
||||||
|
private $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param Message $message
|
||||||
|
*/
|
||||||
|
public function __construct(string $name, Message $message)
|
||||||
|
{
|
||||||
|
parent::__construct($name);
|
||||||
|
$this->message = $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): Message
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,8 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\FilesFinder;
|
namespace LanguageServer\FilesFinder;
|
||||||
|
|
||||||
use LanguageServer\LanguageClient;
|
use LanguageServer\LanguageClient;
|
||||||
use Sabre\Event\Promise;
|
|
||||||
use Sabre\Uri;
|
|
||||||
use Webmozart\Glob\Glob;
|
use Webmozart\Glob\Glob;
|
||||||
|
use function League\Uri\parse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves file content from the client through a textDocument/xcontent request
|
* Retrieves file content from the client through a textDocument/xcontent request
|
||||||
|
@ -31,19 +30,18 @@ class ClientFilesFinder implements FilesFinder
|
||||||
* If the client does not support workspace/files, it falls back to searching the file system directly.
|
* If the client does not support workspace/files, it falls back to searching the file system directly.
|
||||||
*
|
*
|
||||||
* @param string $glob
|
* @param string $glob
|
||||||
* @return Promise <string[]> The URIs
|
* @return \Generator <string[]> 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) {
|
$textDocuments = yield from $this->client->workspace->xfiles();
|
||||||
$uris = [];
|
$uris = [];
|
||||||
foreach ($textDocuments as $textDocument) {
|
foreach ($textDocuments as $textDocument) {
|
||||||
$path = Uri\parse($textDocument->uri)['path'];
|
$path = parse($textDocument->uri)['path'];
|
||||||
if (Glob::match($path, $glob)) {
|
if (Glob::match($path, $glob)) {
|
||||||
$uris[] = $textDocument->uri;
|
$uris[] = $textDocument->uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $uris;
|
return $uris;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\FilesFinder;
|
namespace LanguageServer\FilesFinder;
|
||||||
|
|
||||||
use Webmozart\Glob\Iterator\GlobIterator;
|
use Webmozart\Glob\Glob;
|
||||||
use Sabre\Event\Promise;
|
use function Amp\File\isdir;
|
||||||
use function Sabre\Event\coroutine;
|
use function LanguageServer\{pathToUri};
|
||||||
use function LanguageServer\{pathToUri, timeout};
|
|
||||||
|
|
||||||
class FileSystemFilesFinder implements FilesFinder
|
class FileSystemFilesFinder implements FilesFinder
|
||||||
{
|
{
|
||||||
|
@ -15,21 +14,24 @@ class FileSystemFilesFinder implements FilesFinder
|
||||||
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
|
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
|
||||||
*
|
*
|
||||||
* @param string $glob
|
* @param string $glob
|
||||||
* @return Promise <string[]>
|
* @return \Amp\Promise <string[]>
|
||||||
*/
|
*/
|
||||||
public function find(string $glob): Promise
|
public function find(string $glob): \Generator
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($glob) {
|
|
||||||
$uris = [];
|
$uris = [];
|
||||||
foreach (new GlobIterator($glob) as $path) {
|
$basePath = \Webmozart\Glob\Glob::getBasePath($glob);
|
||||||
// Exclude any directories that also match the glob pattern
|
$pathList = [$basePath];
|
||||||
if (!is_dir($path)) {
|
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;
|
||||||
|
}
|
||||||
|
} elseif (Glob::match($path, $glob)) {
|
||||||
$uris[] = pathToUri($path);
|
$uris[] = pathToUri($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
yield timeout();
|
|
||||||
}
|
}
|
||||||
return $uris;
|
return $uris;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,5 @@ interface FilesFinder
|
||||||
* @param string $glob
|
* @param string $glob
|
||||||
* @return Promise <string[]>
|
* @return Promise <string[]>
|
||||||
*/
|
*/
|
||||||
public function find(string $glob): Promise;
|
public function find(string $glob): \Generator;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,10 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\Index;
|
namespace LanguageServer\Index;
|
||||||
|
|
||||||
use LanguageServer\Definition;
|
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
|
* Returns all indexes managed by the aggregate index
|
||||||
*
|
*
|
||||||
|
@ -29,17 +27,17 @@ abstract class AbstractAggregateIndex implements ReadableIndex
|
||||||
*/
|
*/
|
||||||
protected function registerIndex(ReadableIndex $index)
|
protected function registerIndex(ReadableIndex $index)
|
||||||
{
|
{
|
||||||
$index->on('complete', function () {
|
$index->addListener('complete', function () {
|
||||||
if ($this->isComplete()) {
|
if ($this->isComplete()) {
|
||||||
$this->emit('complete');
|
$this->emit('complete');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$index->on('static-complete', function () {
|
$index->addListener('static-complete', function () {
|
||||||
if ($this->isStaticComplete()) {
|
if ($this->isStaticComplete()) {
|
||||||
$this->emit('static-complete');
|
$this->emit('static-complete');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$index->on('definition-added', function () {
|
$index->addListener('definition-added', function () {
|
||||||
$this->emit('definition-added');
|
$this->emit('definition-added');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,13 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Index;
|
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
|
class DependenciesIndex extends AbstractAggregateIndex
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,15 +5,14 @@ namespace LanguageServer\Index;
|
||||||
|
|
||||||
use Ds\Set;
|
use Ds\Set;
|
||||||
use LanguageServer\Definition;
|
use LanguageServer\Definition;
|
||||||
use Sabre\Event\EmitterTrait;
|
use League\Event\Emitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the index of a project or dependency
|
* Represents the index of a project or dependency
|
||||||
* Serializable for caching
|
* 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
|
* An associative array that maps splitted fully qualified symbol names
|
||||||
|
@ -62,7 +61,6 @@ class Index implements ReadableIndex, \Serializable
|
||||||
$this->setStaticComplete();
|
$this->setStaticComplete();
|
||||||
}
|
}
|
||||||
$this->complete = true;
|
$this->complete = true;
|
||||||
$this->emit('complete');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,7 +71,6 @@ class Index implements ReadableIndex, \Serializable
|
||||||
public function setStaticComplete()
|
public function setStaticComplete()
|
||||||
{
|
{
|
||||||
$this->staticComplete = true;
|
$this->staticComplete = true;
|
||||||
$this->emit('static-complete');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -174,8 +171,6 @@ class Index implements ReadableIndex, \Serializable
|
||||||
{
|
{
|
||||||
$parts = $this->splitFqn($fqn);
|
$parts = $this->splitFqn($fqn);
|
||||||
$this->indexDefinition(0, $parts, $this->definitions, $definition);
|
$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
|
public function getReferenceUris(string $fqn): \Generator
|
||||||
{
|
{
|
||||||
if ($this->references[$fqn]) {
|
if (isset($this->references[$fqn])) {
|
||||||
foreach ($this->references[$fqn] as $uri) {
|
foreach ($this->references[$fqn] as $uri) {
|
||||||
yield $uri;
|
yield $uri;
|
||||||
}
|
}
|
||||||
|
@ -425,7 +420,7 @@ class Index implements ReadableIndex, \Serializable
|
||||||
if (isset($storage[$part])) {
|
if (isset($storage[$part])) {
|
||||||
unset($storage[$part]);
|
unset($storage[$part]);
|
||||||
|
|
||||||
if (0 === count($storage)) {
|
if (0 === count($storage) && $level != 0) {
|
||||||
// parse again the definition tree to remove the parent
|
// parse again the definition tree to remove the parent
|
||||||
// when it has no more children
|
// when it has no more children
|
||||||
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
|
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
|
||||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer\Index;
|
namespace LanguageServer\Index;
|
||||||
|
|
||||||
use LanguageServer\Definition;
|
use LanguageServer\Definition;
|
||||||
use Sabre\Event\EmitterInterface;
|
use League\Event\EmitterInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ReadableIndex interface provides methods to lookup definitions and references
|
* The ReadableIndex interface provides methods to lookup definitions and references
|
||||||
|
|
|
@ -3,13 +3,13 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use Amp\Delayed;
|
||||||
use LanguageServer\Cache\Cache;
|
use LanguageServer\Cache\Cache;
|
||||||
use LanguageServer\FilesFinder\FilesFinder;
|
use LanguageServer\FilesFinder\FilesFinder;
|
||||||
use LanguageServer\Index\{DependenciesIndex, Index};
|
use LanguageServer\Index\{DependenciesIndex, Index};
|
||||||
use LanguageServerProtocol\MessageType;
|
use LanguageServerProtocol\MessageType;
|
||||||
use Webmozart\PathUtil\Path;
|
use Webmozart\PathUtil\Path;
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
|
|
||||||
class Indexer
|
class Indexer
|
||||||
{
|
{
|
||||||
|
@ -100,16 +100,14 @@ class Indexer
|
||||||
*
|
*
|
||||||
* @return Promise <void>
|
* @return Promise <void>
|
||||||
*/
|
*/
|
||||||
public function index(): Promise
|
public function index(): \Generator
|
||||||
{
|
{
|
||||||
return coroutine(function () {
|
|
||||||
|
|
||||||
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
||||||
$uris = yield $this->filesFinder->find($pattern);
|
$uris = yield from $this->filesFinder->find($pattern);
|
||||||
|
|
||||||
$count = count($uris);
|
$count = count($uris);
|
||||||
$startTime = microtime(true);
|
$startTime = microtime(true);
|
||||||
$this->client->window->logMessage(MessageType::INFO, "$count files total");
|
yield from $this->client->window->logMessage(MessageType::INFO, "$count files total");
|
||||||
|
|
||||||
/** @var string[] */
|
/** @var string[] */
|
||||||
$source = [];
|
$source = [];
|
||||||
|
@ -132,16 +130,16 @@ class Indexer
|
||||||
|
|
||||||
// Index source
|
// Index source
|
||||||
// Definitions and static references
|
// Definitions and static references
|
||||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
|
yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
|
||||||
yield $this->indexFiles($source);
|
yield from $this->indexFiles($source);
|
||||||
$this->sourceIndex->setStaticComplete();
|
$this->sourceIndex->setStaticComplete();
|
||||||
// Dynamic references
|
// Dynamic references
|
||||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
|
yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
|
||||||
yield $this->indexFiles($source);
|
yield from $this->indexFiles($source);
|
||||||
$this->sourceIndex->setComplete();
|
$this->sourceIndex->setComplete();
|
||||||
|
|
||||||
// Index dependencies
|
// Index dependencies
|
||||||
$this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages');
|
yield from $this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages');
|
||||||
foreach ($deps as $packageName => $files) {
|
foreach ($deps as $packageName => $files) {
|
||||||
// Find version of package and check cache
|
// Find version of package and check cache
|
||||||
$packageKey = null;
|
$packageKey = null;
|
||||||
|
@ -155,54 +153,54 @@ class Indexer
|
||||||
$packageKey = $packageName . ':' . $packageVersion;
|
$packageKey = $packageName . ':' . $packageVersion;
|
||||||
$cacheKey = self::CACHE_VERSION . ':' . $packageKey;
|
$cacheKey = self::CACHE_VERSION . ':' . $packageKey;
|
||||||
// Check cache
|
// Check cache
|
||||||
$index = yield $this->cache->get($cacheKey);
|
$index = yield from $this->cache->get($cacheKey);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$index = null;
|
||||||
if ($index !== null) {
|
if ($index !== null) {
|
||||||
// Cache hit
|
// Cache hit
|
||||||
$this->dependenciesIndex->setDependencyIndex($packageName, $index);
|
$this->dependenciesIndex->setDependencyIndex($packageName, $index);
|
||||||
$this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache");
|
yield from $this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache");
|
||||||
} else {
|
} else {
|
||||||
// Cache miss
|
// Cache miss
|
||||||
$index = $this->dependenciesIndex->getDependencyIndex($packageName);
|
$index = $this->dependenciesIndex->getDependencyIndex($packageName);
|
||||||
|
|
||||||
// Index definitions and static references
|
// Index definitions and static references
|
||||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references');
|
yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references');
|
||||||
yield $this->indexFiles($files);
|
yield from $this->indexFiles($files);
|
||||||
$index->setStaticComplete();
|
$index->setStaticComplete();
|
||||||
|
|
||||||
// Index dynamic references
|
// Index dynamic references
|
||||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references');
|
yield from $this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references');
|
||||||
yield $this->indexFiles($files);
|
yield from $this->indexFiles($files);
|
||||||
$index->setComplete();
|
$index->setComplete();
|
||||||
|
|
||||||
// If we know the version (cache key), save index for the dependency in the cache
|
// If we know the version (cache key), save index for the dependency in the cache
|
||||||
if ($cacheKey !== null) {
|
if ($cacheKey !== null) {
|
||||||
$this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache");
|
yield from $this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache");
|
||||||
$this->cache->set($cacheKey, $index);
|
yield from $this->cache->set($cacheKey, $index);
|
||||||
} else {
|
} 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);
|
$duration = (int)(microtime(true) - $startTime);
|
||||||
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
||||||
$this->client->window->logMessage(
|
yield from $this->client->window->logMessage(
|
||||||
MessageType::INFO,
|
MessageType::INFO,
|
||||||
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
|
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $files
|
* @param array $files
|
||||||
* @return Promise
|
* @return Promise
|
||||||
*/
|
*/
|
||||||
private function indexFiles(array $files): Promise
|
private function indexFiles(array $files): \Generator
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($files) {
|
|
||||||
foreach ($files as $i => $uri) {
|
foreach ($files as $i => $uri) {
|
||||||
// Skip open documents
|
// Skip open documents
|
||||||
if ($this->documentLoader->isOpen($uri)) {
|
if ($this->documentLoader->isOpen($uri)) {
|
||||||
|
@ -210,22 +208,21 @@ class Indexer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give LS to the chance to handle requests while indexing
|
// Give LS to the chance to handle requests while indexing
|
||||||
yield timeout();
|
yield new Delayed(0);
|
||||||
$this->client->window->logMessage(MessageType::LOG, "Parsing $uri");
|
yield from $this->client->window->logMessage(MessageType::LOG, "Parsing $uri");
|
||||||
try {
|
try {
|
||||||
$document = yield $this->documentLoader->load($uri);
|
$document = yield from $this->documentLoader->load($uri);
|
||||||
if (!isVendored($document, $this->composerJson)) {
|
if (!isVendored($document, $this->composerJson)) {
|
||||||
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
|
yield from $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
|
||||||
}
|
}
|
||||||
} catch (ContentTooLargeException $e) {
|
} catch (ContentTooLargeException $e) {
|
||||||
$this->client->window->logMessage(
|
yield from $this->client->window->logMessage(
|
||||||
MessageType::INFO,
|
MessageType::INFO,
|
||||||
"Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
|
"Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
|
||||||
);
|
);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e);
|
yield from $this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,22 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServerProtocol\{
|
|
||||||
ServerCapabilities,
|
|
||||||
ClientCapabilities,
|
|
||||||
TextDocumentSyncKind,
|
|
||||||
InitializeResult,
|
|
||||||
CompletionOptions,
|
|
||||||
SignatureHelpOptions
|
|
||||||
};
|
|
||||||
use LanguageServer\Message;
|
|
||||||
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 AdvancedJsonRpc;
|
use AdvancedJsonRpc;
|
||||||
use Sabre\Event\Promise;
|
use Amp\Deferred;
|
||||||
use function Sabre\Event\coroutine;
|
use Amp\Delayed;
|
||||||
|
use Amp\Loop;
|
||||||
|
use Amp\Promise;
|
||||||
|
use LanguageServer\Cache\{ClientCache, FileSystemCache};
|
||||||
|
use LanguageServer\ContentRetriever\{ClientContentRetriever, ContentRetriever, FileSystemContentRetriever};
|
||||||
|
use LanguageServer\Event\MessageEvent;
|
||||||
|
use LanguageServer\FilesFinder\{ClientFilesFinder, FilesFinder, FileSystemFilesFinder};
|
||||||
|
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
|
||||||
|
use LanguageServerProtocol\{ClientCapabilities,
|
||||||
|
CompletionOptions,
|
||||||
|
InitializeResult,
|
||||||
|
ServerCapabilities,
|
||||||
|
SignatureHelpOptions,
|
||||||
|
TextDocumentSyncKind};
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Webmozart\PathUtil\Path;
|
use Webmozart\PathUtil\Path;
|
||||||
|
|
||||||
|
@ -38,11 +38,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
*/
|
*/
|
||||||
public $workspace;
|
public $workspace;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Server\Window
|
|
||||||
*/
|
|
||||||
public $window;
|
|
||||||
|
|
||||||
public $telemetry;
|
public $telemetry;
|
||||||
public $completionItem;
|
public $completionItem;
|
||||||
public $codeLens;
|
public $codeLens;
|
||||||
|
@ -106,6 +101,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
*/
|
*/
|
||||||
protected $definitionResolver;
|
protected $definitionResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Deferred
|
||||||
|
*/
|
||||||
|
private $shutdownDeferred;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ProtocolReader $reader
|
* @param ProtocolReader $reader
|
||||||
* @param ProtocolWriter $writer
|
* @param ProtocolWriter $writer
|
||||||
|
@ -113,13 +113,18 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||||
{
|
{
|
||||||
parent::__construct($this, '/');
|
parent::__construct($this, '/');
|
||||||
|
|
||||||
|
$this->shutdownDeferred = new Deferred();
|
||||||
|
|
||||||
$this->protocolReader = $reader;
|
$this->protocolReader = $reader;
|
||||||
$this->protocolReader->on('close', function () {
|
$this->protocolReader->addListener('close', function () {
|
||||||
$this->shutdown();
|
$this->shutdown();
|
||||||
$this->exit();
|
|
||||||
});
|
});
|
||||||
$this->protocolReader->on('message', function (Message $msg) {
|
$this->protocolWriter = $writer;
|
||||||
coroutine(function () use ($msg) {
|
$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
|
// Ignore responses, this is the handler for requests and notifications
|
||||||
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
|
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
|
||||||
return;
|
return;
|
||||||
|
@ -149,12 +154,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
} else {
|
} else {
|
||||||
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
|
$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 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. 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 <InitializeResult>
|
* @return Promise <InitializeResult>
|
||||||
*/
|
*/
|
||||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
|
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
|
||||||
{
|
{
|
||||||
if ($rootPath === null && $rootUri !== null) {
|
$deferred = new Deferred();
|
||||||
$rootPath = uriToPath($rootUri);
|
Loop::defer(function () use ($deferred, $capabilities, $rootPath, $processId, $rootUri) {
|
||||||
}
|
|
||||||
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 {
|
||||||
|
@ -200,25 +206,25 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($rootPath !== null) {
|
if ($rootPath !== null) {
|
||||||
yield $this->beforeIndex($rootPath);
|
yield from $this->beforeIndex($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 from $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $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 from $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 from $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $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 from $this->contentRetriever->retrieve($composerLockFiles[0]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +242,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
$this->composerLock,
|
$this->composerLock,
|
||||||
$this->composerJson
|
$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->xdefinitionProvider = true;
|
||||||
$serverCapabilities->xdependenciesProvider = 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
|
* (otherwise the response might not be delivered correctly to the client). There is a separate exit notification that
|
||||||
* asks the server to exit.
|
* asks the server to exit.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return \Generator
|
||||||
*/
|
*/
|
||||||
public function shutdown()
|
public function shutdown()
|
||||||
{
|
{
|
||||||
unset($this->project);
|
unset($this->project);
|
||||||
}
|
$this->shutdownDeferred->resolve();
|
||||||
|
yield new Delayed(0);
|
||||||
/**
|
|
||||||
* A notification to ask the server to exit its process.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function exit()
|
|
||||||
{
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before indexing, can return a Promise
|
* Called before indexing, can return a Promise
|
||||||
*
|
*
|
||||||
* @param string $rootPath
|
* @param string $rootPath
|
||||||
|
* @return \Generator
|
||||||
*/
|
*/
|
||||||
protected function beforeIndex(string $rootPath)
|
protected function beforeIndex(string $rootPath)
|
||||||
{
|
{
|
||||||
|
yield new Delayed(0, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types = 1);
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use AdvancedJsonRpc\Message as MessageBody;
|
use AdvancedJsonRpc\Message as MessageBody;
|
||||||
use LanguageServer\Message;
|
|
||||||
|
|
||||||
class Message
|
class Message
|
||||||
{
|
{
|
||||||
|
@ -27,7 +26,7 @@ class Message
|
||||||
public static function parse(string $msg): Message
|
public static function parse(string $msg): Message
|
||||||
{
|
{
|
||||||
$obj = new self;
|
$obj = new self;
|
||||||
$parts = explode("\r\n", $msg);
|
$parts = explode("\r\n\r\n", $msg, 2);
|
||||||
$obj->body = MessageBody::parse(array_pop($parts));
|
$obj->body = MessageBody::parse(array_pop($parts));
|
||||||
foreach ($parts as $line) {
|
foreach ($parts as $line) {
|
||||||
if ($line) {
|
if ($line) {
|
||||||
|
@ -55,11 +54,11 @@ class Message
|
||||||
{
|
{
|
||||||
$body = (string)$this->body;
|
$body = (string)$this->body;
|
||||||
$contentLength = strlen($body);
|
$contentLength = strlen($body);
|
||||||
$this->headers['Content-Length'] = $contentLength;
|
$this->headers['Content-Length'] = $contentLength + 6;
|
||||||
$headers = '';
|
$headers = '';
|
||||||
foreach ($this->headers as $name => $value) {
|
foreach ($this->headers as $name => $value) {
|
||||||
$headers .= "$name: $value\r\n";
|
$headers .= "$name: $value\r\n";
|
||||||
}
|
}
|
||||||
return $headers . "\r\n" . $body;
|
return $headers . "\r\n" . $body . "\r\n\r\n\r\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,9 @@ namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\ContentRetriever\ContentRetriever;
|
use LanguageServer\ContentRetriever\ContentRetriever;
|
||||||
use LanguageServer\Index\ProjectIndex;
|
use LanguageServer\Index\ProjectIndex;
|
||||||
use phpDocumentor\Reflection\DocBlockFactory;
|
|
||||||
use Sabre\Event\Promise;
|
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
use Microsoft\PhpParser;
|
use Microsoft\PhpParser;
|
||||||
|
use Microsoft\PhpParser\Parser;
|
||||||
|
use phpDocumentor\Reflection\DocBlockFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes care of loading documents and managing "open" documents
|
* Takes care of loading documents and managing "open" documents
|
||||||
|
@ -18,7 +17,7 @@ class PhpDocumentLoader
|
||||||
/**
|
/**
|
||||||
* A map from URI => PhpDocument of open documents that should be kept in memory
|
* A map from URI => PhpDocument of open documents that should be kept in memory
|
||||||
*
|
*
|
||||||
* @var PhpDocument
|
* @var PhpDocument[]
|
||||||
*/
|
*/
|
||||||
private $documents = [];
|
private $documents = [];
|
||||||
|
|
||||||
|
@ -37,11 +36,6 @@ class PhpDocumentLoader
|
||||||
*/
|
*/
|
||||||
private $parser;
|
private $parser;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var PhpParser\Parser
|
|
||||||
*/
|
|
||||||
private $tolerantParser;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DocBlockFactory
|
* @var DocBlockFactory
|
||||||
*/
|
*/
|
||||||
|
@ -87,11 +81,16 @@ class PhpDocumentLoader
|
||||||
* If the document is not open, loads it.
|
* If the document is not open, loads it.
|
||||||
*
|
*
|
||||||
* @param string $uri
|
* @param string $uri
|
||||||
* @return Promise <PhpDocument>
|
* @return \Generator <PhpDocument>
|
||||||
|
* @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,14 +99,13 @@ class PhpDocumentLoader
|
||||||
* The document is NOT added to the list of open documents, but definitions are registered.
|
* The document is NOT added to the list of open documents, but definitions are registered.
|
||||||
*
|
*
|
||||||
* @param string $uri
|
* @param string $uri
|
||||||
* @return Promise <PhpDocument>
|
* @return \Generator <PhpDocument>
|
||||||
|
* @throws ContentTooLargeException
|
||||||
*/
|
*/
|
||||||
public function load(string $uri): Promise
|
public function load(string $uri): \Generator
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($uri) {
|
|
||||||
|
|
||||||
$limit = 150000;
|
$limit = 150000;
|
||||||
$content = yield $this->contentRetriever->retrieve($uri);
|
$content = yield from $this->contentRetriever->retrieve($uri);
|
||||||
$size = strlen($content);
|
$size = strlen($content);
|
||||||
if ($size > $limit) {
|
if ($size > $limit) {
|
||||||
throw new ContentTooLargeException($uri, $size, $limit);
|
throw new ContentTooLargeException($uri, $size, $limit);
|
||||||
|
@ -120,7 +118,6 @@ class PhpDocumentLoader
|
||||||
$document = $this->create($uri, $content);
|
$document = $this->create($uri, $content);
|
||||||
}
|
}
|
||||||
return $document;
|
return $document;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,7 +144,7 @@ class PhpDocumentLoader
|
||||||
*
|
*
|
||||||
* @param string $uri
|
* @param string $uri
|
||||||
* @param string $content
|
* @param string $content
|
||||||
* @return void
|
* @return PhpDocument
|
||||||
*/
|
*/
|
||||||
public function open(string $uri, string $content)
|
public function open(string $uri, string $content)
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use Sabre\Event\EmitterInterface;
|
use League\Event\EmitterInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must emit a "message" event with a Protocol\Message object as parameter
|
* Must emit a "message" event with a Protocol\Message object as parameter
|
||||||
|
|
|
@ -3,63 +3,52 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\Message;
|
|
||||||
use AdvancedJsonRpc\Message as MessageBody;
|
use AdvancedJsonRpc\Message as MessageBody;
|
||||||
use Sabre\Event\{Loop, Emitter};
|
use Amp\ByteStream\InputStream;
|
||||||
|
use Amp\Loop;
|
||||||
|
use LanguageServer\Event\MessageEvent;
|
||||||
|
use League\Event\Emitter;
|
||||||
|
|
||||||
class ProtocolStreamReader extends Emitter implements ProtocolReader
|
class ProtocolStreamReader extends Emitter implements ProtocolReader
|
||||||
{
|
{
|
||||||
const PARSE_HEADERS = 1;
|
|
||||||
const PARSE_BODY = 2;
|
|
||||||
|
|
||||||
private $input;
|
private $input;
|
||||||
private $parsingMode = self::PARSE_HEADERS;
|
|
||||||
private $buffer = '';
|
|
||||||
private $headers = [];
|
|
||||||
private $contentLength;
|
|
||||||
|
|
||||||
/**
|
public function __construct(InputStream $input)
|
||||||
* @param resource $input
|
|
||||||
*/
|
|
||||||
public function __construct($input)
|
|
||||||
{
|
{
|
||||||
$this->input = $input;
|
$this->input = $input;
|
||||||
|
Loop::defer(function () use (&$input) {
|
||||||
$this->on('close', function () {
|
$buffer = '';
|
||||||
Loop\removeReadStream($this->input);
|
while (true) {
|
||||||
});
|
$headers = [];
|
||||||
|
while (true) {
|
||||||
Loop\addReadStream($this->input, function () {
|
while (($pos = strpos($buffer, "\r\n")) === false) {
|
||||||
if (feof($this->input)) {
|
$read = yield $input->read();
|
||||||
// If stream_select reported a status change for this stream,
|
if ($read === null) {
|
||||||
// but the stream is EOF, it means it was closed.
|
|
||||||
$this->emit('close');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (($c = fgetc($this->input)) !== false && $c !== '') {
|
$buffer .= $read;
|
||||||
$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 = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$headerLine = substr($buffer, 0, $pos);
|
||||||
|
$buffer = substr($buffer, (int)$pos + 2);
|
||||||
|
if (!$headerLine) {
|
||||||
break;
|
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use Amp\ByteStream\OutputStream;
|
||||||
use LanguageServer\Message;
|
use LanguageServer\Message;
|
||||||
use Sabre\Event\{
|
use Sabre\Event\{
|
||||||
Loop,
|
Loop,
|
||||||
|
@ -12,7 +13,7 @@ use Sabre\Event\{
|
||||||
class ProtocolStreamWriter implements ProtocolWriter
|
class ProtocolStreamWriter implements ProtocolWriter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var resource $output
|
* @var OutputStream $output
|
||||||
*/
|
*/
|
||||||
private $output;
|
private $output;
|
||||||
|
|
||||||
|
@ -22,9 +23,9 @@ class ProtocolStreamWriter implements ProtocolWriter
|
||||||
private $messages = [];
|
private $messages = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $output
|
* @param OutputStream $output
|
||||||
*/
|
*/
|
||||||
public function __construct($output)
|
public function __construct(OutputStream $output)
|
||||||
{
|
{
|
||||||
$this->output = $output;
|
$this->output = $output;
|
||||||
}
|
}
|
||||||
|
@ -32,21 +33,9 @@ class ProtocolStreamWriter implements ProtocolWriter
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function write(Message $msg): Promise
|
public function write(Message $msg): \Generator
|
||||||
{
|
{
|
||||||
// if the message queue is currently empty, register a write handler.
|
yield $this->output->write((string)$msg);
|
||||||
if (empty($this->messages)) {
|
|
||||||
Loop\addWriteStream($this->output, function () {
|
|
||||||
$this->flush();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$promise = new Promise();
|
|
||||||
$this->messages[] = [
|
|
||||||
'message' => (string)$msg,
|
|
||||||
'promise' => $promise
|
|
||||||
];
|
|
||||||
return $promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,5 +14,5 @@ interface ProtocolWriter
|
||||||
* @param Message $msg
|
* @param Message $msg
|
||||||
* @return Promise Resolved when the message has been fully written out to the output stream
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,35 +3,33 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
use LanguageServer\{
|
use Amp\Coroutine;
|
||||||
CompletionProvider, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
|
use Amp\Deferred;
|
||||||
};
|
use Amp\Loop;
|
||||||
use LanguageServer\Index\ReadableIndex;
|
use Amp\Promise;
|
||||||
|
use LanguageServer\{CompletionProvider,
|
||||||
|
DefinitionResolver,
|
||||||
|
LanguageClient,
|
||||||
|
PhpDocument,
|
||||||
|
PhpDocumentLoader,
|
||||||
|
SignatureHelpProvider};
|
||||||
use LanguageServer\Factory\LocationFactory;
|
use LanguageServer\Factory\LocationFactory;
|
||||||
use LanguageServer\Factory\RangeFactory;
|
use LanguageServer\Factory\RangeFactory;
|
||||||
use LanguageServerProtocol\{
|
use LanguageServer\Index\ReadableIndex;
|
||||||
FormattingOptions,
|
use LanguageServerProtocol\{CompletionContext,
|
||||||
Hover,
|
Hover,
|
||||||
Location,
|
|
||||||
MarkedString,
|
MarkedString,
|
||||||
|
PackageDescriptor,
|
||||||
Position,
|
Position,
|
||||||
Range,
|
|
||||||
ReferenceContext,
|
ReferenceContext,
|
||||||
SymbolDescriptor,
|
SymbolDescriptor,
|
||||||
PackageDescriptor,
|
|
||||||
SymbolLocationInformation,
|
SymbolLocationInformation,
|
||||||
TextDocumentIdentifier,
|
TextDocumentIdentifier,
|
||||||
TextDocumentItem,
|
TextDocumentItem,
|
||||||
VersionedTextDocumentIdentifier,
|
VersionedTextDocumentIdentifier};
|
||||||
CompletionContext
|
|
||||||
};
|
|
||||||
use Microsoft\PhpParser\Node;
|
use Microsoft\PhpParser\Node;
|
||||||
use Sabre\Event\Promise;
|
use function LanguageServer\{getPackageName, isVendored};
|
||||||
use Sabre\Uri;
|
use function League\Uri\parse;
|
||||||
use function LanguageServer\{
|
|
||||||
isVendored, waitForEvent, getPackageName
|
|
||||||
};
|
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all textDocument/* methods
|
* Provides method handlers for all textDocument/* methods
|
||||||
|
@ -115,13 +113,18 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
|
public function documentSymbol(TextDocumentIdentifier $textDocument): Promise
|
||||||
{
|
{
|
||||||
return $this->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 = [];
|
$symbols = [];
|
||||||
foreach ($document->getDefinitions() as $fqn => $definition) {
|
foreach ($document->getDefinitions() as $fqn => $definition) {
|
||||||
$symbols[] = $definition->symbolInformation;
|
$symbols[] = $definition->symbolInformation;
|
||||||
}
|
}
|
||||||
return $symbols;
|
$deferred->resolve($symbols);
|
||||||
});
|
});
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -134,10 +137,12 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
public function didOpen(TextDocumentItem $textDocument)
|
public function didOpen(TextDocumentItem $textDocument)
|
||||||
{
|
{
|
||||||
|
Loop::defer(function () use ($textDocument) {
|
||||||
$document = $this->documentLoader->open($textDocument->uri, $textDocument->text);
|
$document = $this->documentLoader->open($textDocument->uri, $textDocument->text);
|
||||||
if (!isVendored($document, $this->composerJson)) {
|
if (!isVendored($document, $this->composerJson)) {
|
||||||
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
|
yield from $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,13 +150,18 @@ class TextDocument
|
||||||
*
|
*
|
||||||
* @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument
|
* @param \LanguageServerProtocol\VersionedTextDocumentIdentifier $textDocument
|
||||||
* @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges
|
* @param \LanguageServerProtocol\TextDocumentContentChangeEvent[] $contentChanges
|
||||||
* @return void
|
* @return Promise
|
||||||
*/
|
*/
|
||||||
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
|
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
|
||||||
{
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
Loop::defer(function () use ($deferred, $textDocument, $contentChanges) {
|
||||||
$document = $this->documentLoader->get($textDocument->uri);
|
$document = $this->documentLoader->get($textDocument->uri);
|
||||||
$document->updateContent($contentChanges[0]->text);
|
$document->updateContent($contentChanges[0]->text);
|
||||||
$this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
|
yield from $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics());
|
||||||
|
$deferred->resolve();
|
||||||
|
});
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,8 +189,9 @@ class TextDocument
|
||||||
TextDocumentIdentifier $textDocument,
|
TextDocumentIdentifier $textDocument,
|
||||||
Position $position
|
Position $position
|
||||||
): Promise {
|
): Promise {
|
||||||
return coroutine(function () use ($textDocument, $position) {
|
$deferred = new Deferred();
|
||||||
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
Loop::defer(function () use ($deferred, $textDocument, $position) {
|
||||||
|
$document = yield from $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
$node = $document->getNodeAtPosition($position);
|
$node = $document->getNodeAtPosition($position);
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -189,7 +200,6 @@ class TextDocument
|
||||||
// Variables always stay in the boundary of the file and need to be searched inside their function scope
|
// Variables always stay in the boundary of the file and need to be searched inside their function scope
|
||||||
// by traversing the AST
|
// by traversing the AST
|
||||||
if (
|
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\Parameter
|
||||||
|| $node instanceof Node\UseVariableName
|
|| $node instanceof Node\UseVariableName
|
||||||
|
@ -217,21 +227,18 @@ class TextDocument
|
||||||
// Definition with a global FQN
|
// Definition with a global FQN
|
||||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||||
|
|
||||||
// Wait until indexing finished
|
|
||||||
if (!$this->index->isComplete()) {
|
|
||||||
yield waitForEvent($this->index, 'complete');
|
|
||||||
}
|
|
||||||
if ($fqn === null) {
|
if ($fqn === null) {
|
||||||
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
|
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
|
||||||
if ($fqn === null) {
|
if ($fqn === null) {
|
||||||
return [];
|
$deferred->resolve([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$refDocumentPromises = [];
|
$refDocumentPromises = [];
|
||||||
foreach ($this->index->getReferenceUris($fqn) as $uri) {
|
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) {
|
foreach ($refDocuments as $document) {
|
||||||
$refs = $document->getReferenceNodesByFqn($fqn);
|
$refs = $document->getReferenceNodesByFqn($fqn);
|
||||||
if ($refs !== null) {
|
if ($refs !== null) {
|
||||||
|
@ -241,8 +248,9 @@ class TextDocument
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $locations;
|
$deferred->resolve($locations);
|
||||||
});
|
});
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -256,10 +264,14 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
|
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($textDocument, $position) {
|
$deferred = new Deferred();
|
||||||
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
Loop::defer(function () use ($deferred, $textDocument, $position) {
|
||||||
return $this->signatureHelpProvider->getSignatureHelp($document, $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 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 <Location|Location[]>
|
* @return Promise <Location|Location[]>
|
||||||
|
* @throws \LanguageServer\ContentTooLargeException
|
||||||
*/
|
*/
|
||||||
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
|
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($textDocument, $position) {
|
$deferred = new Deferred();
|
||||||
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
Loop::defer(function () use ($deferred, $textDocument, $position) {
|
||||||
|
$document = yield from $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
$node = $document->getNodeAtPosition($position);
|
$node = $document->getNodeAtPosition($position);
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -291,17 +305,18 @@ class TextDocument
|
||||||
if ($def !== null || $this->index->isComplete()) {
|
if ($def !== null || $this->index->isComplete()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
yield waitForEvent($this->index, 'definition-added');
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
$def === null
|
$def === null
|
||||||
|| $def->symbolInformation === 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
|
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($textDocument, $position) {
|
$deferred = new Deferred();
|
||||||
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
Loop::defer(function () use ($deferred, $textDocument, $position) {
|
||||||
|
$document = yield from $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
// Find the node under the cursor
|
// Find the node under the cursor
|
||||||
$node = $document->getNodeAtPosition($position);
|
$node = $document->getNodeAtPosition($position);
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
|
@ -333,7 +349,6 @@ class TextDocument
|
||||||
if ($def !== null || $this->index->isComplete()) {
|
if ($def !== null || $this->index->isComplete()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
yield waitForEvent($this->index, 'definition-added');
|
|
||||||
}
|
}
|
||||||
$range = RangeFactory::fromNode($node);
|
$range = RangeFactory::fromNode($node);
|
||||||
if ($def === null) {
|
if ($def === null) {
|
||||||
|
@ -346,8 +361,9 @@ class TextDocument
|
||||||
if ($def->documentation) {
|
if ($def->documentation) {
|
||||||
$contents[] = $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
|
* 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.
|
* returned completion item should have the documentation property filled in.
|
||||||
*
|
*
|
||||||
* @param TextDocumentIdentifier The text document
|
* @param TextDocumentIdentifier $textDocument
|
||||||
* @param Position $position The position
|
* @param Position $position The position
|
||||||
* @param CompletionContext|null $context The completion context
|
* @param CompletionContext|null $context The completion context
|
||||||
* @return Promise <CompletionItem[]|CompletionList>
|
* @return Promise <CompletionItem[]|CompletionList>
|
||||||
*/
|
*/
|
||||||
public function completion(TextDocumentIdentifier $textDocument, Position $position, CompletionContext $context = null): Promise
|
public function completion(TextDocumentIdentifier $textDocument, Position $position, CompletionContext $context = null): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($textDocument, $position, $context) {
|
$deferred = new Deferred();
|
||||||
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
Loop::defer(function () use ($deferred, $context, $position, $textDocument) {
|
||||||
return $this->completionProvider->provideCompletion($document, $position, $context);
|
/** @var PhpDocument $document */
|
||||||
|
$document = yield from $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
|
$deferred->resolve($this->completionProvider->provideCompletion($document, $position, $context));
|
||||||
});
|
});
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -387,7 +406,8 @@ class TextDocument
|
||||||
*/
|
*/
|
||||||
public function xdefinition(TextDocumentIdentifier $textDocument, Position $position): 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);
|
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
|
||||||
$node = $document->getNodeAtPosition($position);
|
$node = $document->getNodeAtPosition($position);
|
||||||
if ($node === null) {
|
if ($node === null) {
|
||||||
|
@ -406,12 +426,11 @@ class TextDocument
|
||||||
if ($def !== null || $this->index->isComplete()) {
|
if ($def !== null || $this->index->isComplete()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
yield waitForEvent($this->index, 'definition-added');
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
$def === null
|
$def === null
|
||||||
|| $def->symbolInformation === null
|
|| $def->symbolInformation === null
|
||||||
|| Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
|
|| parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs'
|
||||||
) {
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -422,7 +441,8 @@ class TextDocument
|
||||||
$packageName = $this->composerJson->name;
|
$packageName = $this->composerJson->name;
|
||||||
}
|
}
|
||||||
$descriptor = new SymbolDescriptor($def->fqn, new PackageDescriptor($packageName));
|
$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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,15 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Server;
|
namespace LanguageServer\Server;
|
||||||
|
|
||||||
|
use Amp\Deferred;
|
||||||
|
use Amp\Delayed;
|
||||||
|
use Amp\Loop;
|
||||||
|
use Amp\Promise;
|
||||||
|
use Amp\Success;
|
||||||
use LanguageServer\{LanguageClient, PhpDocumentLoader};
|
use LanguageServer\{LanguageClient, PhpDocumentLoader};
|
||||||
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
|
||||||
use LanguageServer\Factory\LocationFactory;
|
use LanguageServer\Factory\LocationFactory;
|
||||||
use LanguageServerProtocol\{
|
use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex};
|
||||||
FileChangeType,
|
use LanguageServerProtocol\{DependencyReference, FileChangeType, FileEvent, ReferenceInformation, SymbolDescriptor};
|
||||||
FileEvent,
|
|
||||||
SymbolInformation,
|
|
||||||
SymbolDescriptor,
|
|
||||||
ReferenceInformation,
|
|
||||||
DependencyReference,
|
|
||||||
Location
|
|
||||||
};
|
|
||||||
use Sabre\Event\Promise;
|
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
use function LanguageServer\waitForEvent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all workspace/* methods
|
* Provides method handlers for all workspace/* methods
|
||||||
|
@ -83,19 +77,13 @@ class Workspace
|
||||||
*/
|
*/
|
||||||
public function symbol(string $query): Promise
|
public function symbol(string $query): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($query) {
|
|
||||||
// Wait until indexing for definitions finished
|
|
||||||
if (!$this->sourceIndex->isStaticComplete()) {
|
|
||||||
yield waitForEvent($this->sourceIndex, 'static-complete');
|
|
||||||
}
|
|
||||||
$symbols = [];
|
$symbols = [];
|
||||||
foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) {
|
foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) {
|
||||||
if ($query === '' || stripos($fqn, $query) !== false) {
|
if ($query === '' || stripos($fqn, $query) !== false) {
|
||||||
$symbols[] = $definition->symbolInformation;
|
$symbols[] = $definition->symbolInformation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $symbols;
|
return new Success($symbols);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,11 +94,13 @@ class Workspace
|
||||||
*/
|
*/
|
||||||
public function didChangeWatchedFiles(array $changes)
|
public function didChangeWatchedFiles(array $changes)
|
||||||
{
|
{
|
||||||
|
Loop::defer(function () use ($changes) {
|
||||||
foreach ($changes as $change) {
|
foreach ($changes as $change) {
|
||||||
if ($change->type === FileChangeType::DELETED) {
|
if ($change->type === FileChangeType::DELETED) {
|
||||||
$this->client->textDocument->publishDiagnostics($change->uri, []);
|
yield from $this->client->textDocument->publishDiagnostics($change->uri, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,23 +108,21 @@ class Workspace
|
||||||
*
|
*
|
||||||
* @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for.
|
* @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.
|
* @param string[] $files An optional list of files to restrict the search to.
|
||||||
* @return ReferenceInformation[]
|
* @return \Generator
|
||||||
|
* @throws \LanguageServer\ContentTooLargeException
|
||||||
*/
|
*/
|
||||||
public function xreferences($query, array $files = null): Promise
|
public function xreferences($query, array $files = null): \Generator
|
||||||
{
|
{
|
||||||
|
$deferred = new Deferred();
|
||||||
|
Loop::defer(function () use ($deferred, $query, $files) {
|
||||||
// TODO: $files is unused in the coroutine
|
// TODO: $files is unused in the coroutine
|
||||||
return coroutine(function () use ($query, $files) {
|
|
||||||
if ($this->composerLock === null) {
|
if ($this->composerLock === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Wait until indexing finished
|
|
||||||
if (!$this->projectIndex->isComplete()) {
|
|
||||||
yield waitForEvent($this->projectIndex, 'complete');
|
|
||||||
}
|
|
||||||
/** Map from URI to array of referenced FQNs in dependencies */
|
/** Map from URI to array of referenced FQNs in dependencies */
|
||||||
$refs = [];
|
$refs = [];
|
||||||
// Get all references TO dependencies
|
// 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 ($fqns as $fqn) {
|
||||||
foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) {
|
foreach ($this->sourceIndex->getReferenceUris($fqn) as $uri) {
|
||||||
if (!isset($refs[$uri])) {
|
if (!isset($refs[$uri])) {
|
||||||
|
@ -148,7 +136,7 @@ class Workspace
|
||||||
$refInfos = [];
|
$refInfos = [];
|
||||||
foreach ($refs as $uri => $fqns) {
|
foreach ($refs as $uri => $fqns) {
|
||||||
foreach ($fqns as $fqn) {
|
foreach ($fqns as $fqn) {
|
||||||
$doc = yield $this->documentLoader->getOrLoad($uri);
|
$doc = yield from $this->documentLoader->getOrLoad($uri);
|
||||||
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
|
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
|
||||||
$refInfo = new ReferenceInformation;
|
$refInfo = new ReferenceInformation;
|
||||||
$refInfo->reference = LocationFactory::fromNode($node);
|
$refInfo->reference = LocationFactory::fromNode($node);
|
||||||
|
@ -157,8 +145,9 @@ class Workspace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $refInfos;
|
$deferred->resolve($refInfos);
|
||||||
});
|
});
|
||||||
|
return $deferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,14 +3,11 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use Amp\Delayed;
|
||||||
use LanguageServer\Index\ReadableIndex;
|
use LanguageServer\Index\ReadableIndex;
|
||||||
use LanguageServerProtocol\{
|
use LanguageServerProtocol\{Position, SignatureHelp};
|
||||||
Position,
|
|
||||||
SignatureHelp
|
|
||||||
};
|
|
||||||
use Microsoft\PhpParser\Node;
|
use Microsoft\PhpParser\Node;
|
||||||
use Sabre\Event\Promise;
|
use Sabre\Event\Promise;
|
||||||
use function Sabre\Event\coroutine;
|
|
||||||
|
|
||||||
class SignatureHelpProvider
|
class SignatureHelpProvider
|
||||||
{
|
{
|
||||||
|
@ -45,14 +42,13 @@ class SignatureHelpProvider
|
||||||
*
|
*
|
||||||
* @return Promise <SignatureHelp>
|
* @return Promise <SignatureHelp>
|
||||||
*/
|
*/
|
||||||
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
|
// Find the node under the cursor
|
||||||
$node = $doc->getNodeAtPosition($position);
|
$node = $doc->getNodeAtPosition($position);
|
||||||
|
|
||||||
// Find the definition of the item being called
|
// Find the definition of the item being called
|
||||||
list($def, $argumentExpressionList) = yield $this->getCallingInfo($node);
|
list($def, $argumentExpressionList) = yield from $this->getCallingInfo($node);
|
||||||
|
|
||||||
if (!$def || !$def->signatureInformation) {
|
if (!$def || !$def->signatureInformation) {
|
||||||
return new SignatureHelp();
|
return new SignatureHelp();
|
||||||
|
@ -64,7 +60,6 @@ class SignatureHelpProvider
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return new SignatureHelp([$def->signatureInformation], 0, $activeParam);
|
return new SignatureHelp([$def->signatureInformation], 0, $activeParam);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,11 +68,10 @@ class SignatureHelpProvider
|
||||||
*
|
*
|
||||||
* @param Node $node The node to find calling information from
|
* @param Node $node The node to find calling information from
|
||||||
*
|
*
|
||||||
* @return Promise <array|null>
|
* @return \Generator <array|null>
|
||||||
*/
|
*/
|
||||||
private function getCallingInfo(Node $node)
|
private function getCallingInfo(Node $node): \Generator
|
||||||
{
|
{
|
||||||
return coroutine(function () use ($node) {
|
|
||||||
$fqn = null;
|
$fqn = null;
|
||||||
$callingNode = null;
|
$callingNode = null;
|
||||||
if ($node instanceof Node\DelimitedList\ArgumentExpressionList) {
|
if ($node instanceof Node\DelimitedList\ArgumentExpressionList) {
|
||||||
|
@ -133,15 +127,14 @@ class SignatureHelpProvider
|
||||||
if ($def !== null || $this->index->isComplete()) {
|
if ($def !== null || $this->index->isComplete()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
yield waitForEvent($this->index, 'definition-added');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$def) {
|
if (!$def) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield new Delayed(0);
|
||||||
return [$def, $argumentExpressionList];
|
return [$def, $argumentExpressionList];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,10 +3,10 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use Throwable;
|
use Amp\Loop;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Sabre\Event\{Loop, Promise, EmitterInterface};
|
use Throwable;
|
||||||
use Sabre\Uri;
|
use function League\Uri\parse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms an absolute file path into a URI as used by the language server protocol.
|
* Transforms an absolute file path into a URI as used by the language server protocol.
|
||||||
|
@ -60,39 +60,11 @@ function uriToPath(string $uri)
|
||||||
*/
|
*/
|
||||||
function crash(Throwable $err)
|
function crash(Throwable $err)
|
||||||
{
|
{
|
||||||
Loop\nextTick(function () use ($err) {
|
Loop::run(function () use ($err) {
|
||||||
throw $err;
|
throw $err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a promise that is resolved after x seconds.
|
|
||||||
* Useful for giving back control to the event loop inside a coroutine.
|
|
||||||
*
|
|
||||||
* @param int $seconds
|
|
||||||
* @return Promise <void>
|
|
||||||
*/
|
|
||||||
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
|
* Returns the part of $b that is not overlapped by $a
|
||||||
* Example:
|
* Example:
|
||||||
|
@ -125,7 +97,7 @@ function stripStringOverlap(string $a, string $b): string
|
||||||
function sortUrisLevelOrder(&$uriList)
|
function sortUrisLevelOrder(&$uriList)
|
||||||
{
|
{
|
||||||
usort($uriList, function ($a, $b) {
|
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'], '/');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +111,7 @@ function sortUrisLevelOrder(&$uriList)
|
||||||
*/
|
*/
|
||||||
function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool
|
function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool
|
||||||
{
|
{
|
||||||
$path = Uri\parse($document->getUri())['path'];
|
$path = parse($document->getUri())['path'];
|
||||||
$vendorDir = getVendorDir($composerJson);
|
$vendorDir = getVendorDir($composerJson);
|
||||||
return strpos($path, "/$vendorDir/") !== false;
|
return strpos($path, "/$vendorDir/") !== false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue