WIP
parent
347dd14b20
commit
e40336bd5d
|
@ -119,16 +119,32 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
*/
|
||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult
|
||||
{
|
||||
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
||||
$this->rootPath = $rootPath;
|
||||
$this->clientCapabilities = $capabilities;
|
||||
$this->project = new Project($this->client, $capabilities);
|
||||
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
||||
$this->workspace = new Server\Workspace($this->project, $this->client);
|
||||
|
||||
// start building project index
|
||||
if ($rootPath !== null) {
|
||||
$this->indexProject()->otherwise('\\LanguageServer\\crash');
|
||||
$pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath);
|
||||
$uris = yield $this->findFiles($pattern);
|
||||
|
||||
// Find composer.lock files
|
||||
$composerLockFiles = [];
|
||||
$phpFiles = [];
|
||||
foreach ($uris as $uri) {
|
||||
if (Glob::match(Uri\parse($uri)['path'], $composerLockPattern)) {
|
||||
$composerLockFiles[$uri] = json_decode(yield $this->getFileContent($uri));
|
||||
} else {
|
||||
$phpFiles[] = $uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->index($phpFiles)->otherwise('\\LanguageServer\\crash');
|
||||
}
|
||||
|
||||
$this->project = new Project($this->client, $capabilities, $rootPath);
|
||||
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
||||
$this->workspace = new Server\Workspace($this->project, $this->client);
|
||||
|
||||
$serverCapabilities = new ServerCapabilities();
|
||||
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
||||
|
@ -151,6 +167,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||
|
||||
return new InitializeResult($serverCapabilities);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,25 +193,23 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
}
|
||||
|
||||
/**
|
||||
* Parses workspace files, one at a time.
|
||||
* Will read and parse the passed source files in the project and add them to the appropiate indexes
|
||||
*
|
||||
* @return Promise <void>
|
||||
*/
|
||||
private function indexProject(): Promise
|
||||
private function index(array $phpFiles): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
$pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath);
|
||||
$phpPattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
||||
$composerLockPattern = Path::makeAbsolute('**/composer.lock', $this->rootPath);
|
||||
$uris = yield $this->findFiles($pattern);
|
||||
$count = count($uris);
|
||||
|
||||
$count = count($phpFiles);
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
foreach ($uris as $i => $uri) {
|
||||
// Parse PHP files
|
||||
foreach ($phpFiles as $i => $uri) {
|
||||
// Give LS to the chance to handle requests while indexing
|
||||
yield timeout();
|
||||
if (Glob::match())
|
||||
$path = Uri\parse($uri);
|
||||
$this->client->window->logMessage(
|
||||
MessageType::LOG,
|
||||
"Parsing file $i/$count: {$uri}"
|
||||
|
|
|
@ -104,7 +104,8 @@ class PhpDocument
|
|||
/**
|
||||
* @param string $uri The URI of the document
|
||||
* @param string $content The content of the document
|
||||
* @param Project $project The Project this document belongs to (to register definitions etc)
|
||||
* @param Project $project The Project this document belongs to (to load other documents)
|
||||
* @param Index $index The Index to register definitions etc
|
||||
* @param LanguageClient $client The LanguageClient instance (to report errors etc)
|
||||
* @param Parser $parser The PHPParser instance
|
||||
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
|
||||
|
@ -113,6 +114,7 @@ class PhpDocument
|
|||
string $uri,
|
||||
string $content,
|
||||
Project $project,
|
||||
Index $index,
|
||||
LanguageClient $client,
|
||||
Parser $parser,
|
||||
DocBlockFactory $docBlockFactory,
|
||||
|
@ -198,26 +200,26 @@ class PhpDocument
|
|||
// Unregister old definitions
|
||||
if (isset($this->definitions)) {
|
||||
foreach ($this->definitions as $fqn => $definition) {
|
||||
$this->project->removeDefinition($fqn);
|
||||
$this->index->removeDefinition($fqn);
|
||||
}
|
||||
}
|
||||
// Register this document on the project for all the symbols defined in it
|
||||
$this->definitions = $definitionCollector->definitions;
|
||||
$this->definitionNodes = $definitionCollector->nodes;
|
||||
foreach ($definitionCollector->definitions as $fqn => $definition) {
|
||||
$this->project->setDefinition($fqn, $definition);
|
||||
$this->index->setDefinition($fqn, $definition);
|
||||
}
|
||||
|
||||
// Unregister old references
|
||||
if (isset($this->referenceNodes)) {
|
||||
foreach ($this->referenceNodes as $fqn => $node) {
|
||||
$this->project->removeReferenceUri($fqn, $this->uri);
|
||||
$this->index->removeReferenceUri($fqn, $this->uri);
|
||||
}
|
||||
}
|
||||
// Register this document on the project for references
|
||||
$this->referenceNodes = $referencesCollector->nodes;
|
||||
foreach ($referencesCollector->nodes as $fqn => $nodes) {
|
||||
$this->project->addReferenceUri($fqn, $this->uri);
|
||||
$this->index->addReferenceUri($fqn, $this->uri);
|
||||
}
|
||||
|
||||
$this->stmts = $stmts;
|
||||
|
|
178
src/Project.php
178
src/Project.php
|
@ -75,13 +75,31 @@ class Project
|
|||
*/
|
||||
private $clientCapabilities;
|
||||
|
||||
public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities)
|
||||
{
|
||||
private $rootPath;
|
||||
|
||||
private $composerLockFiles;
|
||||
|
||||
/**
|
||||
* @param LanguageClient $client Used for logging and reporting diagnostics
|
||||
* @param ClientCapabilities $clientCapabilities Used for determining the right content/find strategies
|
||||
* @param string|null $rootPath Used for finding files in the project
|
||||
* @param string[] $composerLockFiles An array of URIs of composer.lock files in the project
|
||||
*/
|
||||
public function __construct(
|
||||
LanguageClient $client,
|
||||
ClientCapabilities $clientCapabilities,
|
||||
array $composerLockFiles,
|
||||
string $rootPath = null
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->clientCapabilities = $clientCapabilities;
|
||||
$this->rootPath = $rootPath;
|
||||
$this->parser = new Parser;
|
||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||
$this->definitionResolver = new DefinitionResolver($this);
|
||||
$this->composerLockFiles = $composerLockFiles;
|
||||
// The index for the project itself
|
||||
$this->indexes[''] = new Index;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,20 +138,48 @@ class Project
|
|||
{
|
||||
return coroutine(function () use ($uri) {
|
||||
$limit = 150000;
|
||||
if ($this->clientCapabilities->xcontentProvider) {
|
||||
$content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text;
|
||||
$content = yield $this->getFileContent($uri);
|
||||
$size = strlen($content);
|
||||
if ($size > $limit) {
|
||||
throw new ContentTooLargeException($uri, $size, $limit);
|
||||
}
|
||||
} else {
|
||||
$path = uriToPath($uri);
|
||||
$size = filesize($path);
|
||||
if ($size > $limit) {
|
||||
throw new ContentTooLargeException($uri, $size, $limit);
|
||||
|
||||
/** The key for the index */
|
||||
$key = '';
|
||||
|
||||
// If the document is part of a dependency
|
||||
if (preg_match($u['path'], '/vendor\/(\w+\/\w+)/', $matches)) {
|
||||
if ($this->composerLockFiles === null) {
|
||||
throw new \Exception('composer.lock files were not read yet');
|
||||
}
|
||||
$content = file_get_contents($path);
|
||||
// Try to find closest composer.lock
|
||||
$u = Uri\parse($uri);
|
||||
$packageName = $matches[1];
|
||||
do {
|
||||
$u['path'] = dirname($u['path']);
|
||||
foreach ($this->composerLockFiles as $lockFileUri => $lockFileContent) {
|
||||
$lockFileUri = Uri\parse($composerLockFile);
|
||||
$lockFileUri['path'] = dirname($lockFileUri['path']);
|
||||
if ($u == $lockFileUri) {
|
||||
// Found it, find out package version
|
||||
foreach ($lockFileContent->packages as $package) {
|
||||
if ($package->name === $packageName) {
|
||||
$key = $packageName . ':' . $package->version;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (!empty(trim($u, '/')));
|
||||
}
|
||||
|
||||
// If there is no index for the key yet, create one
|
||||
if (!isset($this->indexes[$key])) {
|
||||
$this->indexes[$key] = new Index;
|
||||
}
|
||||
$index = $this->indexes[$key];
|
||||
|
||||
if (isset($this->documents[$uri])) {
|
||||
$document = $this->documents[$uri];
|
||||
$document->updateContent($content);
|
||||
|
@ -142,6 +188,7 @@ class Project
|
|||
$uri,
|
||||
$content,
|
||||
$this,
|
||||
$index,
|
||||
$this->client,
|
||||
$this->parser,
|
||||
$this->docBlockFactory,
|
||||
|
@ -152,6 +199,25 @@ class Project
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content of a document depending on the client's capabilities
|
||||
*
|
||||
* @param string $uri
|
||||
* @return Promise
|
||||
*/
|
||||
public function getFileContent(string $uri): Promise
|
||||
{
|
||||
if ($this->clientCapabilities->xcontentProvider) {
|
||||
return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))
|
||||
->then(function (TextDocumentItem $textDocumentItem) {
|
||||
return $textDocumentItem->text;
|
||||
});
|
||||
} else {
|
||||
$path = uriToPath($uri);
|
||||
return Promise\resolve(file_get_contents($path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a document is loaded and added to the list of open documents.
|
||||
*
|
||||
|
@ -362,4 +428,96 @@ class Project
|
|||
{
|
||||
return isset($this->definitions[$fqn]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will read and parse all source files in the project and add them to the appropiate indexes
|
||||
*
|
||||
* @return Promise <void>
|
||||
*/
|
||||
private function index(): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
|
||||
$pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath);
|
||||
$phpPattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
||||
$composerLockPattern = Path::makeAbsolute('**/composer.lock', $this->rootPath);
|
||||
|
||||
$uris = yield $this->findFiles($pattern);
|
||||
$count = count($uris);
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
// Find composer.lock files
|
||||
$this->composerLockFiles = [];
|
||||
foreach ($uris as $uri) {
|
||||
if (Glob::match($path, $composerLockPattern)) {
|
||||
$this->composerLockFiles[$uri] = json_decode(yield $this->getFileContent($uri));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PHP files
|
||||
foreach ($uris as $i => $uri) {
|
||||
// Give LS to the chance to handle requests while indexing
|
||||
yield timeout();
|
||||
$path = Uri\parse($uri);
|
||||
if (!Glob::match($path, $phpPattern)) {
|
||||
continue;
|
||||
}
|
||||
$this->client->window->logMessage(
|
||||
MessageType::LOG,
|
||||
"Parsing file $i/$count: {$uri}"
|
||||
);
|
||||
try {
|
||||
yield $this->project->loadDocument($uri);
|
||||
} 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$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."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all PHP files in the workspace.
|
||||
* If the client does not support workspace/files, it falls back to searching the file system directly.
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return Promise <string[]>
|
||||
*/
|
||||
private function findFiles(string $pattern): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
$uris = [];
|
||||
if ($this->clientCapabilities->xfilesProvider) {
|
||||
// Use xfiles request
|
||||
foreach (yield $this->client->workspace->xfiles() as $textDocument) {
|
||||
$path = Uri\parse($textDocument->uri)['path'];
|
||||
if (Glob::match($path, $pattern)) {
|
||||
$uris[] = $textDocument->uri;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use the file system
|
||||
foreach (new GlobIterator($pattern) as $path) {
|
||||
$uris[] = pathToUri($path);
|
||||
yield timeout();
|
||||
}
|
||||
}
|
||||
return $uris;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue