WIP
parent
347dd14b20
commit
e40336bd5d
|
@ -119,38 +119,55 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||||
*/
|
*/
|
||||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult
|
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult
|
||||||
{
|
{
|
||||||
$this->rootPath = $rootPath;
|
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
||||||
$this->clientCapabilities = $capabilities;
|
$this->rootPath = $rootPath;
|
||||||
$this->project = new Project($this->client, $capabilities);
|
$this->clientCapabilities = $capabilities;
|
||||||
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
|
||||||
$this->workspace = new Server\Workspace($this->project, $this->client);
|
|
||||||
|
|
||||||
// start building project index
|
// start building project index
|
||||||
if ($rootPath !== null) {
|
if ($rootPath !== null) {
|
||||||
$this->indexProject()->otherwise('\\LanguageServer\\crash');
|
$pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath);
|
||||||
}
|
$uris = yield $this->findFiles($pattern);
|
||||||
|
|
||||||
$serverCapabilities = new ServerCapabilities();
|
// Find composer.lock files
|
||||||
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
$composerLockFiles = [];
|
||||||
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
$phpFiles = [];
|
||||||
// Support "Find all symbols"
|
foreach ($uris as $uri) {
|
||||||
$serverCapabilities->documentSymbolProvider = true;
|
if (Glob::match(Uri\parse($uri)['path'], $composerLockPattern)) {
|
||||||
// Support "Find all symbols in workspace"
|
$composerLockFiles[$uri] = json_decode(yield $this->getFileContent($uri));
|
||||||
$serverCapabilities->workspaceSymbolProvider = true;
|
} else {
|
||||||
// Support "Format Code"
|
$phpFiles[] = $uri;
|
||||||
$serverCapabilities->documentFormattingProvider = true;
|
}
|
||||||
// Support "Go to definition"
|
}
|
||||||
$serverCapabilities->definitionProvider = true;
|
|
||||||
// Support "Find all references"
|
|
||||||
$serverCapabilities->referencesProvider = true;
|
|
||||||
// Support "Hover"
|
|
||||||
$serverCapabilities->hoverProvider = true;
|
|
||||||
// Support "Completion"
|
|
||||||
$serverCapabilities->completionProvider = new CompletionOptions;
|
|
||||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
|
||||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
|
||||||
|
|
||||||
return new InitializeResult($serverCapabilities);
|
$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)
|
||||||
|
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
||||||
|
// Support "Find all symbols"
|
||||||
|
$serverCapabilities->documentSymbolProvider = true;
|
||||||
|
// Support "Find all symbols in workspace"
|
||||||
|
$serverCapabilities->workspaceSymbolProvider = true;
|
||||||
|
// Support "Format Code"
|
||||||
|
$serverCapabilities->documentFormattingProvider = true;
|
||||||
|
// Support "Go to definition"
|
||||||
|
$serverCapabilities->definitionProvider = true;
|
||||||
|
// Support "Find all references"
|
||||||
|
$serverCapabilities->referencesProvider = true;
|
||||||
|
// Support "Hover"
|
||||||
|
$serverCapabilities->hoverProvider = true;
|
||||||
|
// Support "Completion"
|
||||||
|
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||||
|
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||||
|
$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>
|
* @return Promise <void>
|
||||||
*/
|
*/
|
||||||
private function indexProject(): Promise
|
private function index(array $phpFiles): Promise
|
||||||
{
|
{
|
||||||
return coroutine(function () {
|
return coroutine(function () {
|
||||||
$pattern = Path::makeAbsolute('**/{*.php,composer.lock}', $this->rootPath);
|
|
||||||
$phpPattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
$count = count($phpFiles);
|
||||||
$composerLockPattern = Path::makeAbsolute('**/composer.lock', $this->rootPath);
|
|
||||||
$uris = yield $this->findFiles($pattern);
|
|
||||||
$count = count($uris);
|
|
||||||
|
|
||||||
$startTime = microtime(true);
|
$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
|
// Give LS to the chance to handle requests while indexing
|
||||||
yield timeout();
|
yield timeout();
|
||||||
if (Glob::match())
|
$path = Uri\parse($uri);
|
||||||
$this->client->window->logMessage(
|
$this->client->window->logMessage(
|
||||||
MessageType::LOG,
|
MessageType::LOG,
|
||||||
"Parsing file $i/$count: {$uri}"
|
"Parsing file $i/$count: {$uri}"
|
||||||
|
|
|
@ -104,7 +104,8 @@ class PhpDocument
|
||||||
/**
|
/**
|
||||||
* @param string $uri The URI of the document
|
* @param string $uri The URI of the document
|
||||||
* @param string $content The content 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 LanguageClient $client The LanguageClient instance (to report errors etc)
|
||||||
* @param Parser $parser The PHPParser instance
|
* @param Parser $parser The PHPParser instance
|
||||||
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
|
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
|
||||||
|
@ -113,6 +114,7 @@ class PhpDocument
|
||||||
string $uri,
|
string $uri,
|
||||||
string $content,
|
string $content,
|
||||||
Project $project,
|
Project $project,
|
||||||
|
Index $index,
|
||||||
LanguageClient $client,
|
LanguageClient $client,
|
||||||
Parser $parser,
|
Parser $parser,
|
||||||
DocBlockFactory $docBlockFactory,
|
DocBlockFactory $docBlockFactory,
|
||||||
|
@ -198,26 +200,26 @@ class PhpDocument
|
||||||
// Unregister old definitions
|
// Unregister old definitions
|
||||||
if (isset($this->definitions)) {
|
if (isset($this->definitions)) {
|
||||||
foreach ($this->definitions as $fqn => $definition) {
|
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
|
// Register this document on the project for all the symbols defined in it
|
||||||
$this->definitions = $definitionCollector->definitions;
|
$this->definitions = $definitionCollector->definitions;
|
||||||
$this->definitionNodes = $definitionCollector->nodes;
|
$this->definitionNodes = $definitionCollector->nodes;
|
||||||
foreach ($definitionCollector->definitions as $fqn => $definition) {
|
foreach ($definitionCollector->definitions as $fqn => $definition) {
|
||||||
$this->project->setDefinition($fqn, $definition);
|
$this->index->setDefinition($fqn, $definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister old references
|
// Unregister old references
|
||||||
if (isset($this->referenceNodes)) {
|
if (isset($this->referenceNodes)) {
|
||||||
foreach ($this->referenceNodes as $fqn => $node) {
|
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
|
// Register this document on the project for references
|
||||||
$this->referenceNodes = $referencesCollector->nodes;
|
$this->referenceNodes = $referencesCollector->nodes;
|
||||||
foreach ($referencesCollector->nodes as $fqn => $nodes) {
|
foreach ($referencesCollector->nodes as $fqn => $nodes) {
|
||||||
$this->project->addReferenceUri($fqn, $this->uri);
|
$this->index->addReferenceUri($fqn, $this->uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->stmts = $stmts;
|
$this->stmts = $stmts;
|
||||||
|
|
188
src/Project.php
188
src/Project.php
|
@ -75,13 +75,31 @@ class Project
|
||||||
*/
|
*/
|
||||||
private $clientCapabilities;
|
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->client = $client;
|
||||||
$this->clientCapabilities = $clientCapabilities;
|
$this->clientCapabilities = $clientCapabilities;
|
||||||
|
$this->rootPath = $rootPath;
|
||||||
$this->parser = new Parser;
|
$this->parser = new Parser;
|
||||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||||
$this->definitionResolver = new DefinitionResolver($this);
|
$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) {
|
return coroutine(function () use ($uri) {
|
||||||
$limit = 150000;
|
$limit = 150000;
|
||||||
if ($this->clientCapabilities->xcontentProvider) {
|
$content = yield $this->getFileContent($uri);
|
||||||
$content = (yield $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri)))->text;
|
$size = strlen($content);
|
||||||
$size = strlen($content);
|
if ($size > $limit) {
|
||||||
if ($size > $limit) {
|
throw new ContentTooLargeException($uri, $size, $limit);
|
||||||
throw new ContentTooLargeException($uri, $size, $limit);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$path = uriToPath($uri);
|
|
||||||
$size = filesize($path);
|
|
||||||
if ($size > $limit) {
|
|
||||||
throw new ContentTooLargeException($uri, $size, $limit);
|
|
||||||
}
|
|
||||||
$content = file_get_contents($path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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');
|
||||||
|
}
|
||||||
|
// 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])) {
|
if (isset($this->documents[$uri])) {
|
||||||
$document = $this->documents[$uri];
|
$document = $this->documents[$uri];
|
||||||
$document->updateContent($content);
|
$document->updateContent($content);
|
||||||
|
@ -142,6 +188,7 @@ class Project
|
||||||
$uri,
|
$uri,
|
||||||
$content,
|
$content,
|
||||||
$this,
|
$this,
|
||||||
|
$index,
|
||||||
$this->client,
|
$this->client,
|
||||||
$this->parser,
|
$this->parser,
|
||||||
$this->docBlockFactory,
|
$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.
|
* Ensures a document is loaded and added to the list of open documents.
|
||||||
*
|
*
|
||||||
|
@ -362,4 +428,96 @@ class Project
|
||||||
{
|
{
|
||||||
return isset($this->definitions[$fqn]);
|
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