1
0
Fork 0
php-language-server/src/LanguageServer.php

238 lines
8.3 KiB
PHP
Raw Normal View History

2016-08-22 15:32:31 +00:00
<?php
declare(strict_types = 1);
2016-08-22 15:32:31 +00:00
namespace LanguageServer;
use LanguageServer\Protocol\{
ServerCapabilities,
ClientCapabilities,
TextDocumentSyncKind,
Message,
MessageType,
2016-10-20 01:48:30 +00:00
InitializeResult,
2016-11-06 13:47:36 +00:00
SymbolInformation,
TextDocumentIdentifier
};
2016-10-20 01:31:12 +00:00
use AdvancedJsonRpc;
2016-11-06 13:47:36 +00:00
use Sabre\Event\{Loop, Promise};
2016-10-30 23:06:36 +00:00
use function Sabre\Event\coroutine;
2016-10-19 10:41:53 +00:00
use Exception;
2016-11-06 13:47:36 +00:00
use RuntimeException;
2016-10-20 01:31:12 +00:00
use Throwable;
2016-11-03 10:52:51 +00:00
use Webmozart\Glob\Iterator\GlobIterator;
2016-11-06 13:47:36 +00:00
use Webmozart\PathUtil\Path;
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
{
/**
* Handles textDocument/* method calls
*
* @var Server\TextDocument
*/
2016-08-25 13:27:14 +00:00
public $textDocument;
/**
* 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;
2016-10-30 23:06:36 +00:00
/**
* ClientCapabilities
*/
private $clientCapabilities;
2016-08-25 13:27:14 +00:00
private $protocolReader;
private $protocolWriter;
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;
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;
2016-10-31 10:47:21 +00:00
$this->protocolReader->on('message', function (Message $msg) {
2016-11-03 09:37:19 +00:00
coroutine(function () use ($msg) {
// Ignore responses, this is the handler for requests and notifications
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
return;
2016-10-20 01:36:03 +00:00
}
2016-11-03 09:37:19 +00:00
$result = null;
$error = null;
try {
// Invoke the method handler to get a result
2016-11-06 14:22:32 +00:00
$result = yield $this->dispatch($msg->body);
2016-11-03 09:37:19 +00:00
} catch (AdvancedJsonRpc\Error $e) {
// If a ResponseError is thrown, send it back in the Response
$error = $e;
} catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response
2016-11-06 17:41:42 +00:00
$error = new AdvancedJsonRpc\Error(
$e->getMessage(),
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null,
$e
);
2016-11-03 09:37:19 +00:00
}
// Only send a Response for a Request
// Notifications do not send Responses
if (AdvancedJsonRpc\Request::isRequest($msg->body)) {
if ($error !== null) {
$responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error);
} else {
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
}
$this->protocolWriter->write(new Message($responseBody));
}
2016-11-06 13:47:36 +00:00
})->otherwise('\\LanguageServer\\crash');
2016-08-25 13:27:14 +00:00
});
$this->protocolWriter = $writer;
2016-10-31 10:47:21 +00:00
$this->client = new LanguageClient($reader, $writer);
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)
* @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
*/
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-11-03 09:37:19 +00:00
$this->clientCapabilities = $capabilities;
2016-11-06 13:47:36 +00:00
$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);
2016-10-20 01:48:30 +00:00
// start building project index
2016-10-20 01:48:30 +00:00
if ($rootPath !== null) {
2016-11-06 13:47:36 +00:00
$this->indexProject()->otherwise('\\LanguageServer\\crash');
}
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;
// 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;
// Support "Find all references"
$serverCapabilities->referencesProvider = true;
// 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-11-06 13:47:36 +00:00
unset($this->project);
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
}
/**
* Parses workspace files, one at a time.
*
2016-11-03 09:37:19 +00:00
* @return Promise <void>
*/
2016-11-06 17:36:14 +00:00
private function indexProject(): Promise
{
2016-11-03 09:37:19 +00:00
return coroutine(function () {
2016-11-06 22:54:52 +00:00
$textDocuments = yield $this->globWorkspace(['**/*.php']);
2016-11-06 13:47:36 +00:00
$count = count($textDocuments);
2016-10-30 23:06:36 +00:00
$startTime = microtime(true);
2016-11-06 17:36:14 +00:00
yield Promise\all(array_map(function ($textDocument, $i) use ($count) {
return coroutine(function () use ($textDocument, $i, $count) {
// Give LS to the chance to handle requests while indexing
yield timeout();
$this->client->window->logMessage(
MessageType::INFO,
"Parsing file $i/$count: {$textDocument->uri}"
);
try {
yield $this->project->loadDocument($textDocument->uri);
} catch (Exception $e) {
$this->client->window->logMessage(
MessageType::ERROR,
2016-11-07 12:16:14 +00:00
"Error parsing file {$textDocument->uri}: " . (string)$e
2016-11-06 17:36:14 +00:00
);
}
});
}, $textDocuments, array_keys($textDocuments)));
2016-10-30 23:06:36 +00:00
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
2016-11-07 11:26:32 +00:00
$this->client->window->logMessage(
MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
2016-10-30 23:06:36 +00:00
});
}
2016-11-03 10:52:51 +00:00
/**
* Returns all files matching a glob pattern.
* If the client does not support workspace/xglob, it falls back to globbing the file system directly.
*
2016-11-06 22:54:52 +00:00
* @param string $patterns
2016-11-03 10:52:51 +00:00
* @return Promise <TextDocumentIdentifier[]>
*/
2016-11-06 22:54:52 +00:00
private function globWorkspace(array $patterns): Promise
2016-11-03 10:52:51 +00:00
{
if ($this->clientCapabilities->xglobProvider) {
// Use xglob request
2016-11-06 22:54:52 +00:00
return $this->client->workspace->xglob($patterns);
2016-11-03 10:52:51 +00:00
} else {
// Use the file system
2016-11-07 11:26:32 +00:00
$textDocuments = [];
2016-11-06 22:54:52 +00:00
return Promise\all(array_map(function ($pattern) use (&$textDocuments) {
return coroutine(function () use ($pattern, &$textDocuments) {
2016-11-07 00:23:38 +00:00
$pattern = Path::makeAbsolute($pattern, $this->rootPath);
foreach (new GlobIterator($pattern) as $path) {
$textDocuments[] = new TextDocumentIdentifier(pathToUri($path));
yield timeout();
}
2016-11-06 22:54:52 +00:00
});
2016-11-07 11:26:32 +00:00
}, $patterns))->then(function () use (&$textDocuments) {
2016-11-06 13:47:36 +00:00
return $textDocuments;
});
2016-11-03 10:52:51 +00:00
}
}
2016-08-22 15:32:31 +00:00
}