2016-08-22 15:32:31 +00:00
|
|
|
<?php
|
2016-09-30 09:54:49 +00:00
|
|
|
declare(strict_types = 1);
|
2016-08-22 15:32:31 +00:00
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
2016-09-02 19:13:30 +00:00
|
|
|
use LanguageServer\Server\TextDocument;
|
2016-09-30 09:30:08 +00:00
|
|
|
use LanguageServer\Protocol\{
|
|
|
|
ServerCapabilities,
|
|
|
|
ClientCapabilities,
|
|
|
|
TextDocumentSyncKind,
|
|
|
|
Message,
|
|
|
|
MessageType,
|
2016-10-20 01:48:30 +00:00
|
|
|
InitializeResult,
|
|
|
|
SymbolInformation
|
2016-09-30 09:30:08 +00:00
|
|
|
};
|
2016-10-20 01:31:12 +00:00
|
|
|
use AdvancedJsonRpc;
|
2016-09-30 09:30:08 +00:00
|
|
|
use Sabre\Event\Loop;
|
2016-10-20 01:48:30 +00:00
|
|
|
use JsonMapper;
|
2016-10-19 10:41:53 +00:00
|
|
|
use Exception;
|
2016-10-20 01:31:12 +00:00
|
|
|
use Throwable;
|
2016-08-22 15:32:31 +00:00
|
|
|
|
2016-10-20 01:31:12 +00:00
|
|
|
class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
2016-08-22 15:32:31 +00:00
|
|
|
{
|
2016-09-02 19:13:30 +00:00
|
|
|
/**
|
|
|
|
* Handles textDocument/* method calls
|
|
|
|
*
|
|
|
|
* @var Server\TextDocument
|
|
|
|
*/
|
2016-08-25 13:27:14 +00:00
|
|
|
public $textDocument;
|
2016-09-02 19:13:30 +00:00
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
/**
|
|
|
|
* Handles workspace/* method calls
|
|
|
|
*
|
|
|
|
* @var Server\Workspace
|
|
|
|
*/
|
|
|
|
public $workspace;
|
|
|
|
|
2016-08-25 13:27:14 +00:00
|
|
|
public $telemetry;
|
|
|
|
public $window;
|
|
|
|
public $completionItem;
|
|
|
|
public $codeLens;
|
|
|
|
|
|
|
|
private $protocolReader;
|
|
|
|
private $protocolWriter;
|
2016-09-02 19:13:30 +00:00
|
|
|
private $client;
|
2016-08-25 13:27:14 +00:00
|
|
|
|
2016-10-20 01:48:30 +00:00
|
|
|
/**
|
|
|
|
* The root project path that was passed to initialize()
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $rootPath;
|
2016-09-30 09:30:08 +00:00
|
|
|
private $project;
|
|
|
|
|
2016-08-25 13:27:14 +00:00
|
|
|
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
2016-08-22 15:32:31 +00:00
|
|
|
{
|
2016-08-25 13:27:14 +00:00
|
|
|
parent::__construct($this, '/');
|
|
|
|
$this->protocolReader = $reader;
|
|
|
|
$this->protocolReader->onMessage(function (Message $msg) {
|
2016-10-20 01:36:03 +00:00
|
|
|
$result = null;
|
|
|
|
$error = null;
|
2016-08-25 13:27:14 +00:00
|
|
|
try {
|
|
|
|
// Invoke the method handler to get a result
|
|
|
|
$result = $this->dispatch($msg->body);
|
2016-10-20 01:36:03 +00:00
|
|
|
} catch (AdvancedJsonRpc\Error $e) {
|
|
|
|
// If a ResponseError is thrown, send it back in the Response
|
|
|
|
$error = $e;
|
2016-08-25 13:27:14 +00:00
|
|
|
} catch (Throwable $e) {
|
2016-10-20 01:31:12 +00:00
|
|
|
// If an unexpected error occured, send back an INTERNAL_ERROR error response
|
|
|
|
$error = new AdvancedJsonRpc\Error($e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e);
|
2016-08-25 13:27:14 +00:00
|
|
|
}
|
|
|
|
// Only send a Response for a Request
|
|
|
|
// Notifications do not send Responses
|
2016-10-20 01:31:12 +00:00
|
|
|
if (AdvancedJsonRpc\Request::isRequest($msg->body)) {
|
2016-10-20 01:36:03 +00:00
|
|
|
if ($error !== null) {
|
|
|
|
$responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error);
|
|
|
|
} else {
|
|
|
|
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
|
|
|
|
}
|
2016-10-20 01:31:12 +00:00
|
|
|
$this->protocolWriter->write(new Message($responseBody));
|
2016-08-25 13:27:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
$this->protocolWriter = $writer;
|
2016-09-02 19:13:30 +00:00
|
|
|
$this->client = new LanguageClient($writer);
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
$this->project = new Project($this->client);
|
|
|
|
|
|
|
|
$this->textDocument = new Server\TextDocument($this->project, $this->client);
|
|
|
|
$this->workspace = new Server\Workspace($this->project, $this->client);
|
2016-08-22 15:32:31 +00:00
|
|
|
}
|
2016-08-22 21:48:20 +00:00
|
|
|
|
2016-08-25 13:27:14 +00:00
|
|
|
/**
|
|
|
|
* The initialize request is sent as the first request from the client to the server.
|
|
|
|
*
|
|
|
|
* @param int $processId The process Id of the parent process that started the server.
|
|
|
|
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
|
2016-09-30 09:30:08 +00:00
|
|
|
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
|
2016-08-25 13:27:14 +00:00
|
|
|
* @return InitializeResult
|
|
|
|
*/
|
2016-09-30 09:30:08 +00:00
|
|
|
public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult
|
2016-08-23 09:21:37 +00:00
|
|
|
{
|
2016-10-20 01:48:30 +00:00
|
|
|
$this->rootPath = $rootPath;
|
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
// start building project index
|
2016-10-20 01:48:30 +00:00
|
|
|
if ($rootPath !== null) {
|
|
|
|
$this->restoreCache();
|
|
|
|
$this->indexProject();
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
|
2016-08-25 13:27:14 +00:00
|
|
|
$serverCapabilities = new ServerCapabilities();
|
2016-08-23 09:21:37 +00:00
|
|
|
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
2016-08-25 13:27:14 +00:00
|
|
|
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
2016-08-23 09:21:37 +00:00
|
|
|
// Support "Find all symbols"
|
2016-08-25 13:27:14 +00:00
|
|
|
$serverCapabilities->documentSymbolProvider = true;
|
2016-09-30 09:30:08 +00:00
|
|
|
// Support "Find all symbols in workspace"
|
|
|
|
$serverCapabilities->workspaceSymbolProvider = true;
|
2016-09-06 10:54:34 +00:00
|
|
|
// Support "Format Code"
|
|
|
|
$serverCapabilities->documentFormattingProvider = true;
|
2016-10-08 12:59:08 +00:00
|
|
|
// Support "Go to definition"
|
|
|
|
$serverCapabilities->definitionProvider = true;
|
2016-10-11 23:45:15 +00:00
|
|
|
// Support "Find all references"
|
|
|
|
$serverCapabilities->referencesProvider = true;
|
2016-10-19 10:31:32 +00:00
|
|
|
// Support "Hover"
|
|
|
|
$serverCapabilities->hoverProvider = true;
|
2016-10-08 12:59:08 +00:00
|
|
|
|
2016-08-25 13:27:14 +00:00
|
|
|
return new InitializeResult($serverCapabilities);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit
|
|
|
|
* (otherwise the response might not be delivered correctly to the client). There is a separate exit notification that
|
|
|
|
* asks the server to exit.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function shutdown()
|
|
|
|
{
|
2016-10-20 01:48:30 +00:00
|
|
|
if ($this->rootPath !== null) {
|
|
|
|
$this->saveCache();
|
|
|
|
}
|
2016-08-25 13:27:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A notification to ask the server to exit its process.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function exit()
|
|
|
|
{
|
|
|
|
exit(0);
|
2016-08-23 09:21:37 +00:00
|
|
|
}
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses workspace files, one at a time.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2016-10-20 01:48:30 +00:00
|
|
|
private function indexProject()
|
2016-09-30 09:30:08 +00:00
|
|
|
{
|
2016-10-20 01:48:30 +00:00
|
|
|
$fileList = findFilesRecursive($this->rootPath, '/^.+\.php$/i');
|
2016-09-30 09:30:08 +00:00
|
|
|
$numTotalFiles = count($fileList);
|
|
|
|
|
|
|
|
$startTime = microtime(true);
|
|
|
|
$fileNum = 0;
|
|
|
|
|
2016-10-20 01:48:30 +00:00
|
|
|
$processFile = function() use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) {
|
2016-09-30 09:30:08 +00:00
|
|
|
if ($fileNum < $numTotalFiles) {
|
|
|
|
$file = $fileList[$fileNum];
|
|
|
|
$uri = pathToUri($file);
|
|
|
|
$fileNum++;
|
2016-10-20 01:48:30 +00:00
|
|
|
$shortName = substr($file, strlen($this->rootPath) + 1);
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
|
2016-09-30 09:30:08 +00:00
|
|
|
|
2016-10-14 07:06:56 +00:00
|
|
|
if (filesize($file) > 500000) {
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB");
|
|
|
|
} else {
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
|
2016-10-19 10:41:53 +00:00
|
|
|
try {
|
|
|
|
$this->project->loadDocument($uri);
|
|
|
|
} catch (Exception $e) {
|
|
|
|
$this->client->window->logMessage(MessageType::ERROR, "Error parsing file $shortName: " . $e->getMessage());
|
|
|
|
}
|
2016-10-14 07:06:56 +00:00
|
|
|
}
|
2016-09-30 09:30:08 +00:00
|
|
|
|
2016-10-20 01:48:30 +00:00
|
|
|
if ($fileNum % 1000 === 0) {
|
|
|
|
$this->saveCache();
|
|
|
|
}
|
|
|
|
|
2016-09-30 09:30:08 +00:00
|
|
|
Loop\setTimeout($processFile, 0);
|
|
|
|
} else {
|
|
|
|
$duration = (int)(microtime(true) - $startTime);
|
|
|
|
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated.");
|
2016-10-20 01:48:30 +00:00
|
|
|
$this->saveCache();
|
2016-09-30 09:30:08 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Loop\setTimeout($processFile, 0);
|
|
|
|
}
|
2016-10-20 01:48:30 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Restores the definition and reference index from the .phpls cache directory, if available
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function restoreCache()
|
|
|
|
{
|
|
|
|
$cacheDir = $this->rootPath . '/.phpls';
|
|
|
|
if (is_dir($cacheDir)) {
|
|
|
|
if (file_exists($cacheDir . '/symbols.json')) {
|
|
|
|
$json = json_decode(file_get_contents($cacheDir . '/symbols.json'));
|
|
|
|
$mapper = new JsonMapper;
|
|
|
|
$symbols = $mapper->mapArray($json, [], SymbolInformation::class);
|
|
|
|
$count = count($symbols);
|
|
|
|
$this->project->setSymbols($symbols);
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Restoring $count symbols");
|
|
|
|
}
|
|
|
|
if (file_exists($cacheDir . '/references.json')) {
|
|
|
|
$references = json_decode(file_get_contents($cacheDir . '/references.json'), true);
|
|
|
|
$count = array_sum(array_map('count', $references));
|
|
|
|
$this->project->setReferenceUris($references);
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Restoring $count references");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, 'No cache found');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Saves the definition and reference index to the .phpls cache directory
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function saveCache()
|
|
|
|
{
|
|
|
|
// Cache definitions, references
|
|
|
|
$cacheDir = $this->rootPath . '/.phpls';
|
|
|
|
if (!is_dir($cacheDir)) {
|
|
|
|
mkdir($cacheDir);
|
|
|
|
}
|
|
|
|
|
|
|
|
$symbols = $this->project->getSymbols();
|
|
|
|
$count = count($symbols);
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Saving $count symbols to cache");
|
|
|
|
file_put_contents($cacheDir . "/symbols.json", json_encode($symbols, JSON_UNESCAPED_SLASHES));
|
|
|
|
|
|
|
|
$references = $this->project->getReferenceUris();
|
|
|
|
$count = array_sum(array_map('count', $references));
|
|
|
|
$this->client->window->logMessage(MessageType::INFO, "Saving $count references to cache");
|
|
|
|
file_put_contents($cacheDir . "/references.json", json_encode($references, JSON_UNESCAPED_SLASHES));
|
|
|
|
}
|
2016-08-22 15:32:31 +00:00
|
|
|
}
|