1
0
Fork 0
pull/277/head v4.3.0
Felix Becker 2017-02-04 00:20:38 +01:00 committed by GitHub
parent 34d3d2030d
commit bedd157636
11 changed files with 496 additions and 84 deletions

29
src/Cache/Cache.php Normal file
View File

@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use Sabre\Event\Promise;
/**
* A key/value store for caching purposes
*/
interface Cache
{
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise;
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise;
}

48
src/Cache/ClientCache.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise;
/**
* Caches content through a xcache/* requests
*/
class ClientCache implements Cache
{
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
return $this->client->xcache->get($key)->then('unserialize')->otherwise(function () {
// Ignore
});
}
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise
{
return $this->client->xcache->set($key, serialize($value))->otherwise(function () {
// Ignore
});
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise;
/**
* Caches content on the file system
*/
class FileSystemCache implements Cache
{
/**
* @var string
*/
public $cacheDir;
public function __construct()
{
if (PHP_OS === 'WINNT') {
$this->cacheDir = getenv('LOCALAPPDATA') . '\\PHP Language Server\\';
} else if (getenv('XDG_CACHE_HOME')) {
$this->cacheDir = getenv('XDG_CACHE_HOME') . '/phpls/';
} else {
$this->cacheDir = getenv('HOME') . '/.phpls/';
}
}
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
try {
$file = $this->cacheDir . urlencode($key);
if (!file_exists($file)) {
return Promise\resolve(null);
}
return Promise\resolve(unserialize(file_get_contents($file)));
} catch (\Exception $e) {
return Promise\resolve(null);
}
}
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise
{
try {
$file = $this->cacheDir . urlencode($key);
if (!file_exists($this->cacheDir)) {
mkdir($this->cacheDir);
}
file_put_contents($file, serialize($value));
} finally {
return Promise\resolve(null);
}
}
}

43
src/Client/XCache.php Normal file
View File

@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use Sabre\Event\Promise;
/**
* Provides method handlers for all xcache/* methods
*/
class XCache
{
/**
* @var ClientHandler
*/
private $handler;
public function __construct(ClientHandler $handler)
{
$this->handler = $handler;
}
/**
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
return $this->handler->request('xcache/get', ['key' => $key]);
}
/**
* @param string $key
* @param mixed $value
* @return Promise <mixed>
*/
public function set(string $key, $value): Promise
{
return $this->handler->notify('xcache/set', ['key' => $key, 'value' => $value]);
}
}

View File

@ -34,6 +34,17 @@ class DependenciesIndex extends AbstractAggregateIndex
return $this->indexes[$packageName]; return $this->indexes[$packageName];
} }
/**
* @param string $packageName
* @param Index $index
* @return void
*/
public function setDependencyIndex(string $packageName, Index $index)
{
$this->indexes[$packageName] = $index;
$this->registerIndex($index);
}
/** /**
* @param string $packageName * @param string $packageName
* @return void * @return void

View File

@ -10,7 +10,7 @@ use Sabre\Event\EmitterTrait;
* 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 class Index implements ReadableIndex, \Serializable
{ {
use EmitterTrait; use EmitterTrait;
@ -185,4 +185,30 @@ class Index implements ReadableIndex
} }
array_splice($this->references[$fqn], $index, 1); array_splice($this->references[$fqn], $index, 1);
} }
/**
* @param string $serialized
* @return void
*/
public function unserialize($serialized)
{
$data = unserialize($serialized);
foreach ($data as $prop => $val) {
$this->$prop = $val;
}
}
/**
* @param string $serialized
* @return string
*/
public function serialize()
{
return serialize([
'definitions' => $this->definitions,
'references' => $this->references,
'complete' => $this->complete,
'staticComplete' => $this->staticComplete
]);
}
} }

222
src/Indexer.php Normal file
View File

@ -0,0 +1,222 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Cache\Cache;
use LanguageServer\FilesFinder\FilesFinder;
use LanguageServer\Index\{DependenciesIndex, Index};
use LanguageServer\Protocol\MessageType;
use Webmozart\PathUtil\Path;
use Composer\Semver\VersionParser;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class Indexer
{
/**
* @var The prefix for every cache item
*/
const CACHE_VERSION = 1;
/**
* @var FilesFinder
*/
private $filesFinder;
/**
* @var string
*/
private $rootPath;
/**
* @var LanguageClient
*/
private $client;
/**
* @var Cache
*/
private $cache;
/**
* @var DependenciesIndex
*/
private $dependenciesIndex;
/**
* @var Index
*/
private $sourceIndex;
/**
* @var PhpDocumentLoader
*/
private $documentLoader;
/**
* @var \stdClasss
*/
private $composerLock;
/**
* @param FilesFinder $filesFinder
* @param string $rootPath
* @param LanguageClient $client
* @param Cache $cache
* @param DependenciesIndex $dependenciesIndex
* @param Index $sourceIndex
* @param PhpDocumentLoader $documentLoader
* @param \stdClass|null $composerLock
*/
public function __construct(
FilesFinder $filesFinder,
string $rootPath,
LanguageClient $client,
Cache $cache,
DependenciesIndex $dependenciesIndex,
Index $sourceIndex,
PhpDocumentLoader $documentLoader,
\stdClass $composerLock = null
) {
$this->filesFinder = $filesFinder;
$this->rootPath = $rootPath;
$this->client = $client;
$this->cache = $cache;
$this->dependenciesIndex = $dependenciesIndex;
$this->sourceIndex = $sourceIndex;
$this->documentLoader = $documentLoader;
$this->composerLock = $composerLock;
}
/**
* Will read and parse the passed source files in the project and add them to the appropiate indexes
*
* @return Promise <void>
*/
public function index(): Promise
{
return coroutine(function () {
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
$uris = yield $this->filesFinder->find($pattern);
$count = count($uris);
$startTime = microtime(true);
$this->client->window->logMessage(MessageType::INFO, "$count files total");
/** @var string[] */
$source = [];
/** @var string[][] */
$deps = [];
foreach ($uris as $uri) {
if ($this->composerLock !== null && preg_match('/\/vendor\/([^\/]+\/[^\/]+)\//', $uri, $matches)) {
// Dependency file
$packageName = $matches[1];
if (!isset($deps[$packageName])) {
$deps[$packageName] = [];
}
$deps[$packageName][] = $uri;
} else {
// Source file
$source[] = $uri;
}
}
// Index source
// Definitions and static references
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
yield $this->indexFiles($source);
$this->sourceIndex->setStaticComplete();
// Dynamic references
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
yield $this->indexFiles($source);
$this->sourceIndex->setComplete();
// Index dependencies
$this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages');
foreach ($deps as $packageName => $files) {
// Find version of package and check cache
$packageKey = null;
$cacheKey = null;
$index = null;
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
// Check if package name matches and version is absolute
// Dynamic constraints are not cached, because they can change every time
$packageVersion = ltrim($package->version, 'v');
if ($package->name === $packageName && strpos($packageVersion, 'dev') === false) {
$packageKey = $packageName . ':' . $packageVersion;
$cacheKey = self::CACHE_VERSION . ':' . $packageKey;
// Check cache
$index = yield $this->cache->get($cacheKey);
break;
}
}
if ($index !== null) {
// Cache hit
$this->dependenciesIndex->setDependencyIndex($packageName, $index);
$this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache");
} else {
// Cache miss
$index = $this->dependenciesIndex->getDependencyIndex($packageName);
// Index definitions and static references
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references');
yield $this->indexFiles($files);
$index->setStaticComplete();
// Index dynamic references
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references');
yield $this->indexFiles($files);
$index->setComplete();
// If we know the version (cache key), save index for the dependency in the cache
if ($cacheKey !== null) {
$this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache");
$this->cache->set($cacheKey, $index);
}
}
}
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(
MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
});
}
/**
* @param array $files
* @return Promise
*/
private function indexFiles(array $files): Promise
{
return coroutine(function () use ($files) {
foreach ($files as $i => $uri) {
// Skip open documents
if ($this->documentLoader->isOpen($uri)) {
continue;
}
// Give LS to the chance to handle requests while indexing
yield timeout();
$this->client->window->logMessage(MessageType::LOG, "Parsing $uri");
try {
$document = yield $this->documentLoader->load($uri);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
}
} catch (ContentTooLargeException $e) {
$this->client->window->logMessage(
MessageType::INFO,
"Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
);
} catch (\Exception $e) {
$this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e);
}
}
});
}
}

View File

@ -28,6 +28,13 @@ class LanguageClient
*/ */
public $workspace; public $workspace;
/**
* Handles xcache/* methods
*
* @var Client\XCache
*/
public $xcache;
public function __construct(ProtocolReader $reader, ProtocolWriter $writer) public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{ {
$handler = new ClientHandler($reader, $writer); $handler = new ClientHandler($reader, $writer);
@ -36,5 +43,6 @@ class LanguageClient
$this->textDocument = new Client\TextDocument($handler, $mapper); $this->textDocument = new Client\TextDocument($handler, $mapper);
$this->window = new Client\Window($handler); $this->window = new Client\Window($handler);
$this->workspace = new Client\Workspace($handler, $mapper); $this->workspace = new Client\Workspace($handler, $mapper);
$this->xcache = new Client\XCache($handler);
} }
} }

View File

@ -17,13 +17,13 @@ use LanguageServer\Protocol\{
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise}; use Sabre\Event\{Loop, Promise};
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Exception; use Exception;
use Throwable; use Throwable;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Webmozart\Glob\Glob;
use Sabre\Uri; use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher class LanguageServer extends AdvancedJsonRpc\Dispatcher
@ -202,23 +202,39 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
if ($rootPath !== null) { if ($rootPath !== null) {
yield $this->beforeIndex($rootPath); yield $this->beforeIndex($rootPath);
$this->index($rootPath)->otherwise('\\LanguageServer\\crash');
// Find composer.json
if ($this->composerJson === null) {
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
if (!empty($composerJsonFiles)) {
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
}
}
// Find composer.lock
if ($this->composerLock === null) {
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
}
}
$cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
// Index in background
$indexer = new Indexer(
$this->filesFinder,
$rootPath,
$this->client,
$cache,
$dependenciesIndex,
$sourceIndex,
$this->documentLoader,
$this->composerLock
);
$indexer->index()->otherwise('\\LanguageServer\\crash');
} }
// Find composer.json
if ($this->composerJson === null) {
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
if (!empty($composerJsonFiles)) {
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
}
}
// Find composer.lock
if ($this->composerLock === null) {
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
if (!empty($composerLockFiles)) {
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
}
}
if ($this->textDocument === null) { if ($this->textDocument === null) {
$this->textDocument = new Server\TextDocument( $this->textDocument = new Server\TextDocument(
@ -298,66 +314,4 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
protected function beforeIndex(string $rootPath) protected function beforeIndex(string $rootPath)
{ {
} }
/**
* Will read and parse the passed source files in the project and add them to the appropiate indexes
*
* @param string $rootPath
* @return Promise <void>
*/
protected function index(string $rootPath): Promise
{
return coroutine(function () use ($rootPath) {
$pattern = Path::makeAbsolute('**/*.php', $rootPath);
$uris = yield $this->filesFinder->find($pattern);
$count = count($uris);
$startTime = microtime(true);
foreach (['Collecting definitions and static references', 'Collecting dynamic references'] as $run => $text) {
$this->client->window->logMessage(MessageType::INFO, $text);
foreach ($uris as $i => $uri) {
if ($this->documentLoader->isOpen($uri)) {
continue;
}
// Give LS to the chance to handle requests while indexing
yield timeout();
$this->client->window->logMessage(
MessageType::LOG,
"Parsing file $i/$count: {$uri}"
);
try {
$document = yield $this->documentLoader->load($uri);
if (!$document->isVendored()) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
}
} catch (ContentTooLargeException $e) {
$this->client->window->logMessage(
MessageType::INFO,
"Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
);
} catch (Exception $e) {
$this->client->window->logMessage(
MessageType::ERROR,
"Error parsing file {$uri}: " . (string)$e
);
}
}
if ($run === 0) {
$this->projectIndex->setStaticComplete();
} else {
$this->projectIndex->setComplete();
}
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(
MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
}
});
}
} }

View File

@ -17,4 +17,11 @@ class ClientCapabilities
* @var bool|null * @var bool|null
*/ */
public $xcontentProvider; public $xcontentProvider;
/**
* The client supports xcache/* requests
*
* @var bool|null
*/
public $xcacheProvider;
} }

View File

@ -104,11 +104,7 @@ class LanguageServerTest extends TestCase
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} }
} else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) { } else if (strpos($msg->body->params->message, 'All 25 PHP files parsed') !== false) {
if ($run === 1) { $promise->fulfill();
$run++;
} else {
$promise->fulfill();
}
} }
} }
}); });