1
0
Fork 0
pull/214/head
Felix Becker 2016-12-08 01:22:41 +01:00
parent 347dd14b20
commit e40336bd5d
3 changed files with 233 additions and 58 deletions

View File

@ -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}"

View File

@ -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;

View File

@ -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;
});
}
} }