Update
parent
138b529df1
commit
41ad025fe7
|
@ -1,5 +1,7 @@
|
||||||
# PHP Language Server
|
# PHP Language Server
|
||||||
> A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
|
|
||||||
|
|
||||||
[]() []()
|
[](https://packagist.org/packages/felixfbecker/language-server)
|
||||||
|
[](https://travis-ci.org/felixfbecker/php-language-server)
|
||||||
|
[](https://packagist.org/packages/felixfbecker/language-server)
|
||||||
|
|
||||||
|
A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
|
||||||
|
|
|
@ -5,5 +5,6 @@ use Sabre\Event\Loop;
|
||||||
|
|
||||||
require __DIR__ . '../vendor/autoload.php';
|
require __DIR__ . '../vendor/autoload.php';
|
||||||
|
|
||||||
$server = new LanguageServer(STDIN, STDOUT);
|
$server = new LanguageServer(new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT));
|
||||||
$server->run();
|
|
||||||
|
Loop\run();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "language-server",
|
"name": "felixfbecker/language-server",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "PHP Implementation of the Visual Studio Code Language Server Protocol",
|
"description": "PHP Implementation of the Visual Studio Code Language Server Protocol",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@ -23,14 +23,15 @@
|
||||||
"refactor"
|
"refactor"
|
||||||
],
|
],
|
||||||
"bin": ["bin/main.php"],
|
"bin": ["bin/main.php"],
|
||||||
"minimum-stability": "dev",
|
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0",
|
"php": ">=7.0",
|
||||||
"nikic/php-parser": "3.0.0alpha1",
|
"nikic/php-parser": "3.0.0alpha1",
|
||||||
"phpdocumentor/reflection-docblock": "^3.0",
|
"phpdocumentor/reflection-docblock": "^3.0",
|
||||||
"sabre/event": "^3.0",
|
"sabre/event": "^3.0",
|
||||||
"netresearch/jsonmapper": "^0.11.0"
|
"felixfbecker/advanced-json-rpc": "^1.2"
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"LanguageServer\\": "src/"
|
"LanguageServer\\": "src/"
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
|
||||||
|
class ColumnCalculator extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
private $code;
|
||||||
|
private $codeLength;
|
||||||
|
|
||||||
|
public function __construct($code)
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
$this->codeLength = strlen($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
$startFilePos = $node->getAttribute('startFilePos');
|
||||||
|
$endFilePos = $node->getAttribute('endFilePos');
|
||||||
|
|
||||||
|
if ($startFilePos > $this->codeLength || $endFilePos > $this->codeLength) {
|
||||||
|
throw new \RuntimeException('Invalid position information');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startLinePos = strrpos($this->code, "\n", $startFilePos - $this->codeLength);
|
||||||
|
if ($startLinePos === false) {
|
||||||
|
$startLinePos = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endLinePos = strrpos($this->code, "\n", $endFilePos - $this->codeLength);
|
||||||
|
if ($endLinePos === false) {
|
||||||
|
$endLinePos = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->setAttribute('startColumn', $startFilePos - $startLinePos);
|
||||||
|
$node->setAttribute('endColumn', $endFilePos - $endLinePos);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,32 +2,91 @@
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
use LanguageServer\Protocol\{ProtocolServer, ServerCapabilities, TextDocumentSyncKind};
|
use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message};
|
||||||
use LanguageServer\Protocol\Methods\{InitializeParams, InitializeResult};
|
use LanguageServer\Protocol\InitializeResult;
|
||||||
|
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody};
|
||||||
|
|
||||||
class LanguageServer extends ProtocolServer
|
class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||||
{
|
{
|
||||||
protected $textDocument;
|
public $textDocument;
|
||||||
protected $telemetry;
|
public $telemetry;
|
||||||
protected $window;
|
public $window;
|
||||||
protected $workspace;
|
public $workspace;
|
||||||
protected $completionItem;
|
public $completionItem;
|
||||||
protected $codeLens;
|
public $codeLens;
|
||||||
|
|
||||||
public function __construct($input, $output)
|
private $protocolReader;
|
||||||
|
private $protocolWriter;
|
||||||
|
|
||||||
|
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||||
{
|
{
|
||||||
parent::__construct($input, $output);
|
parent::__construct($this, '/');
|
||||||
|
$this->protocolReader = $reader;
|
||||||
|
$this->protocolReader->onMessage(function (Message $msg) {
|
||||||
|
$err = null;
|
||||||
|
$result = null;
|
||||||
|
try {
|
||||||
|
// Invoke the method handler to get a result
|
||||||
|
$result = $this->dispatch($msg->body);
|
||||||
|
} catch (ResponseError $e) {
|
||||||
|
// If a ResponseError is thrown, send it back in the Response (result will be null)
|
||||||
|
$err = $e;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// If an unexpected error occured, send back an INTERNAL_ERROR error response (result will be null)
|
||||||
|
$err = new ResponseError(
|
||||||
|
$e->getMessage(),
|
||||||
|
$e->getCode() === 0 ? ErrorCode::INTERNAL_ERROR : $e->getCode(),
|
||||||
|
null,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Only send a Response for a Request
|
||||||
|
// Notifications do not send Responses
|
||||||
|
if (RequestBody::isRequest($msg->body)) {
|
||||||
|
$this->protocolWriter->write(new Message(new ResponseBody($msg->body->id, $result, $err)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->protocolWriter = $writer;
|
||||||
$this->textDocument = new TextDocumentManager();
|
$this->textDocument = new TextDocumentManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function initialize(InitializeParams $req): InitializeResult
|
/**
|
||||||
|
* The initialize request is sent as the first request from the client to the server.
|
||||||
|
*
|
||||||
|
* @param string $rootPath The rootPath of the workspace. Is null if no folder is open.
|
||||||
|
* @param int $processId The process Id of the parent process that started the server.
|
||||||
|
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
|
||||||
|
* @return InitializeResult
|
||||||
|
*/
|
||||||
|
public function initialize(string $rootPath, int $processId, ClientCapabilities $capabilities): InitializeResult
|
||||||
{
|
{
|
||||||
$capabilities = new ServerCapabilites();
|
$serverCapabilities = new ServerCapabilities();
|
||||||
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
|
||||||
$capabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
||||||
// Support "Find all symbols"
|
// Support "Find all symbols"
|
||||||
$capabilities->documentSymbolProvider = true;
|
$serverCapabilities->documentSymbolProvider = true;
|
||||||
$result = new InitializeResult($capabilities);
|
return new InitializeResult($serverCapabilities);
|
||||||
return $result;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification to ask the server to exit its process.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function exit()
|
||||||
|
{
|
||||||
|
exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
class ClientCapabilites
|
class ClientCapabilities
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
|
class InitializeResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The capabilities the language server provides.
|
||||||
|
*
|
||||||
|
* @var LanguageServer\Protocol\ServerCapabilities
|
||||||
|
*/
|
||||||
|
public $capabilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LanguageServer\Protocol\ServerCapabilities $capabilities
|
||||||
|
*/
|
||||||
|
public function __construct(ServerCapabilities $capabilities = null)
|
||||||
|
{
|
||||||
|
$this->capabilities = $capabilities ?? new ServerCapabilities();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,4 +16,10 @@ class Location
|
||||||
* @var Range
|
* @var Range
|
||||||
*/
|
*/
|
||||||
public $range;
|
public $range;
|
||||||
|
|
||||||
|
public function __construct(string $uri = null, Range $range = null)
|
||||||
|
{
|
||||||
|
$this->uri = $uri;
|
||||||
|
$this->range = $range;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,64 @@
|
||||||
<?php
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
/**
|
use AdvancedJsonRpc\Message as MessageBody;
|
||||||
* A general message as defined by JSON-RPC. The language server protocol always uses "2.0" as the jsonrpc version.
|
|
||||||
*/
|
class Message
|
||||||
abstract class Message
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The version (2.0)
|
* @var \AdvancedJsonRpc\Message
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
public $jsonrpc;
|
public $body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a request body and returns the appropiate Message subclass
|
* @var string[]
|
||||||
*
|
|
||||||
* @param string $body The raw request body
|
|
||||||
* @param string $fallbackClass The class to fall back to if the body is not a Notification and the method is
|
|
||||||
* unknown (Request::class or Response::class)
|
|
||||||
*/
|
*/
|
||||||
public static function parse(string $body, string $fallbackClass): self
|
public $headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a message
|
||||||
|
*
|
||||||
|
* @param string $msg
|
||||||
|
* @return Message
|
||||||
|
*/
|
||||||
|
public static function parse(string $msg): Message
|
||||||
{
|
{
|
||||||
$decoded = json_decode($body);
|
$obj = new self;
|
||||||
|
$parts = explode("\r\n", $msg);
|
||||||
// The appropiate Request/Notification subclasses are namespaced according to the method
|
$obj->body = MessageBody::parse(array_pop($parts));
|
||||||
// example: textDocument/didOpen -> LanguageServer\Protocol\Methods\TextDocument\DidOpenNotification
|
foreach ($parts as $line) {
|
||||||
$class = __NAMESPACE__ . '\\Methods\\' . implode('\\', array_map('ucfirst', explode('/', $decoded->method))) . (isset($decoded->id) ? 'Request' : 'Notification');
|
if ($line) {
|
||||||
|
$pair = explode(': ', $line);
|
||||||
// If the Request/Notification type is unknown, instantiate a basic Request or Notification class
|
$obj->headers[$pair[0]] = $pair[1];
|
||||||
// (this is the reason Request and Notification are not abstract)
|
|
||||||
if (!class_exists($class)) {
|
|
||||||
fwrite(STDERR, "Unknown method {$decoded->method}\n");
|
|
||||||
if (!isset($decoded->id)) {
|
|
||||||
$class = Notification::class;
|
|
||||||
} else {
|
|
||||||
$class = $fallbackClass;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
// JsonMapper will take care of recursively using the right classes for $params etc.
|
/**
|
||||||
$mapper = new JsonMapper();
|
* @param \AdvancedJsonRpc\Message $body
|
||||||
$message = $mapper->map($decoded, new $class);
|
* @param string[] $headers
|
||||||
|
*/
|
||||||
|
public function __construct(MessageBody $body = null, array $headers = [])
|
||||||
|
{
|
||||||
|
$this->body = $body;
|
||||||
|
if (!isset($headers['Content-Type'])) {
|
||||||
|
$headers['Content-Type'] = 'application/vscode-jsonrpc; charset=utf8';
|
||||||
|
}
|
||||||
|
$this->headers = $headers;
|
||||||
|
}
|
||||||
|
|
||||||
return $message;
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$body = (string)$this->body;
|
||||||
|
$contentLength = strlen($body);
|
||||||
|
$this->headers['Content-Length'] = $contentLength;
|
||||||
|
$headers = '';
|
||||||
|
foreach ($this->headers as $name => $value) {
|
||||||
|
$headers .= "$name: $value\r\n";
|
||||||
|
}
|
||||||
|
return $headers . "\r\n" . $body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace LanguageServer\Protocol\Methods\Initialize;
|
|
||||||
|
|
||||||
use LanguageServer\Protocol\Result;
|
|
||||||
use LanguageServer\Protocol\ServerCapabilities;
|
|
||||||
|
|
||||||
class InitializeResult extends Result
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The capabilities the language server provides.
|
|
||||||
*
|
|
||||||
* @var LanguageServer\Protocol\ServerCapabilities
|
|
||||||
*/
|
|
||||||
public $capabilites;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param LanguageServer\Protocol\ServerCapabilities $capabilites
|
|
||||||
*/
|
|
||||||
public function __construct(ServerCapabilities $capabilites = null)
|
|
||||||
{
|
|
||||||
$this->capabilities = $capabilites ?? new ServerCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,4 +20,10 @@ class Position
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
public $character;
|
public $character;
|
||||||
|
|
||||||
|
public function __construct(int $line = null, int $character = null)
|
||||||
|
{
|
||||||
|
$this->line = $line;
|
||||||
|
$this->character = $character;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
|
||||||
|
|
||||||
use Sabre\Event\Loop;
|
|
||||||
use LanguageServer\Protocol\Methods\{InitializeRequest, InitializeResponse};
|
|
||||||
|
|
||||||
abstract class ParsingMode {
|
|
||||||
const HEADERS = 1;
|
|
||||||
const BODY = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ProtocolServer
|
|
||||||
{
|
|
||||||
private $input;
|
|
||||||
private $output;
|
|
||||||
private $buffer = '';
|
|
||||||
private $parsingMode = ParsingMode::HEADERS;
|
|
||||||
private $headers = [];
|
|
||||||
private $contentLength = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param resource $input The input stream
|
|
||||||
* @param resource $output The output stream
|
|
||||||
*/
|
|
||||||
public function __construct($input, $output)
|
|
||||||
{
|
|
||||||
$this->input = $input;
|
|
||||||
$this->output = $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an event loop and listens on the provided input stream for messages, invoking method handlers and
|
|
||||||
* responding on the provided output stream
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function listen()
|
|
||||||
{
|
|
||||||
Loop\addReadStream($this->input, function() {
|
|
||||||
$this->buffer .= fgetc($this->input);
|
|
||||||
switch ($parsingMode) {
|
|
||||||
case ParsingMode::HEADERS:
|
|
||||||
if (substr($buffer, -4) === '\r\n\r\n') {
|
|
||||||
$this->parsingMode = ParsingMode::BODY;
|
|
||||||
$this->contentLength = (int)$headers['Content-Length'];
|
|
||||||
$this->buffer = '';
|
|
||||||
} else if (substr($buffer, -2) === '\r\n') {
|
|
||||||
$parts = explode(': ', $buffer);
|
|
||||||
$headers[$parts[0]] = $parts[1];
|
|
||||||
$this->buffer = '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ParsingMode::BODY:
|
|
||||||
if (strlen($buffer) === $contentLength) {
|
|
||||||
$msg = Message::parse($body, Request::class);
|
|
||||||
$result = null;
|
|
||||||
$err = null;
|
|
||||||
try {
|
|
||||||
// Invoke the method handler to get a result
|
|
||||||
$result = $this->dispatch($msg);
|
|
||||||
} catch (ResponseError $e) {
|
|
||||||
// If a ResponseError is thrown, send it back in the Response (result will be null)
|
|
||||||
$err = $e;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
// If an unexpected error occured, send back an INTERNAL_ERROR error response (result will be null)
|
|
||||||
$err = new ResponseError(
|
|
||||||
$e->getMessage(),
|
|
||||||
$e->getCode() === 0 ? ErrorCode::INTERNAL_ERROR : $e->getCode(),
|
|
||||||
null,
|
|
||||||
$e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Only send a Response for a Request
|
|
||||||
// Notifications do not send Responses
|
|
||||||
if ($msg instanceof Request) {
|
|
||||||
$this->send(new Response($msg->id, $msg->method, $result, $err));
|
|
||||||
}
|
|
||||||
$this->parsingMode = ParsingMode::HEADERS;
|
|
||||||
$this->buffer = '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Loop\run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the appropiate method handler for an incoming Message
|
|
||||||
*
|
|
||||||
* @param Message $msg The incoming message
|
|
||||||
* @return Result|void
|
|
||||||
*/
|
|
||||||
private function dispatch(Message $msg)
|
|
||||||
{
|
|
||||||
// Find out the object and function that should be called
|
|
||||||
$obj = $this;
|
|
||||||
$parts = explode('/', $msg->method);
|
|
||||||
// The function to call is always the last part of the method
|
|
||||||
$fn = array_pop($parts);
|
|
||||||
// For namespaced methods like textDocument/didOpen, call the didOpen method on the $textDocument property
|
|
||||||
// For simple methods like initialize, shutdown, exit, this loop will simply not be entered and $obj will be
|
|
||||||
// the server ($this)
|
|
||||||
foreach ($parts as $part) {
|
|
||||||
if (!isset($obj->$part)) {
|
|
||||||
throw new ResponseError("Method {$msg->method} is not implemented", ErrorCode::METHOD_NOT_FOUND);
|
|
||||||
}
|
|
||||||
$obj = $obj->$part;
|
|
||||||
}
|
|
||||||
// Check if $fn exists on $obj
|
|
||||||
if (!method_exists($obj, $fn)) {
|
|
||||||
throw new ResponseError("Method {$msg->method} is not implemented", ErrorCode::METHOD_NOT_FOUND);
|
|
||||||
}
|
|
||||||
// Invoke the method handler and return the result
|
|
||||||
return $obj->$fn($msg->params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a Message to the client (for example a Response)
|
|
||||||
*
|
|
||||||
* @param Message $msg
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private function send(Message $msg)
|
|
||||||
{
|
|
||||||
fwrite($this->output, json_encode($msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initialize request is sent as the first request from the client to the server.
|
|
||||||
* The default implementation returns no capabilities.
|
|
||||||
*
|
|
||||||
* @param LanguageServer\Protocol\Methods\InitializeParams $params
|
|
||||||
* @return LanguageServer\Protocol\Methods\IntializeResult
|
|
||||||
*/
|
|
||||||
protected function initialize(InitializeParams $params): InitializeResult
|
|
||||||
{
|
|
||||||
return new InitializeResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
* The default implementation does nothing.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function shutdown()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A notification to ask the server to exit its process.
|
|
||||||
* The default implementation does exactly this.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function exit()
|
|
||||||
{
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,4 +20,10 @@ class Range
|
||||||
* @var Position
|
* @var Position
|
||||||
*/
|
*/
|
||||||
public $end;
|
public $end;
|
||||||
|
|
||||||
|
public function __construct(Position $start = null, Position $end = null)
|
||||||
|
{
|
||||||
|
$this->start = $start;
|
||||||
|
$this->end = $end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A request message to describe a request between the client and the server. Every processed request must send a
|
|
||||||
* response back to the sender of the request.
|
|
||||||
*/
|
|
||||||
class Request extends Message
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var int|string
|
|
||||||
*/
|
|
||||||
public $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Params
|
|
||||||
*/
|
|
||||||
public $params;
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
|
||||||
|
|
||||||
class Response extends Message
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var int|string
|
|
||||||
*/
|
|
||||||
public $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var mixed
|
|
||||||
*/
|
|
||||||
public $result;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ResponseError|null
|
|
||||||
*/
|
|
||||||
public $error;
|
|
||||||
|
|
||||||
public function __construct($id, string $method, $result, ResponseError $error = null)
|
|
||||||
{
|
|
||||||
$this->result = $result;
|
|
||||||
$this->error = $error;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ResponseError extends Exception
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A number indicating the error type that occurred.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
public $code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A string providing a short description of the error.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Primitive or Structured value that contains additional information about the
|
|
||||||
* error. Can be omitted.
|
|
||||||
*
|
|
||||||
* @var mixed
|
|
||||||
*/
|
|
||||||
public $data;
|
|
||||||
|
|
||||||
public function __construct(string $message, int $code = ErrorCode::INTERNAL_ERROR, $data = null, Throwable $previous = null)
|
|
||||||
{
|
|
||||||
parent::__construct($message, $code, $previous);
|
|
||||||
$this->data = $data;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace LanguageServer\Protocol;
|
namespace LanguageServer\Protocol;
|
||||||
|
|
||||||
class ServerCapabilites
|
class ServerCapabilities
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Defines how text documents are synced.
|
* Defines how text documents are synced.
|
||||||
|
|
|
@ -11,7 +11,7 @@ abstract class SymbolKind
|
||||||
const MODULE = 2;
|
const MODULE = 2;
|
||||||
const NAMESPACE = 3;
|
const NAMESPACE = 3;
|
||||||
const PACKAGE = 4;
|
const PACKAGE = 4;
|
||||||
const _CLASS = 5;
|
const CLASS_ = 5;
|
||||||
const METHOD = 6;
|
const METHOD = 6;
|
||||||
const PROPERTY = 7;
|
const PROPERTY = 7;
|
||||||
const FIELD = 8;
|
const FIELD = 8;
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
interface ProtocolReader
|
||||||
|
{
|
||||||
|
public function onMessage(callable $listener);
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use LanguageServer\Protocol\Message;
|
||||||
|
use AdvancedJsonRpc\Message as MessageBody;
|
||||||
|
use Sabre\Event\EventEmitter;
|
||||||
|
use Sabre\Event\Loop;
|
||||||
|
|
||||||
|
abstract class ParsingMode
|
||||||
|
{
|
||||||
|
const HEADERS = 1;
|
||||||
|
const BODY = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtocolStreamReader implements ProtocolReader
|
||||||
|
{
|
||||||
|
private $input;
|
||||||
|
private $parsingMode = ParsingMode::HEADERS;
|
||||||
|
private $buffer = '';
|
||||||
|
private $headers = [];
|
||||||
|
private $contentLength;
|
||||||
|
private $listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $input
|
||||||
|
*/
|
||||||
|
public function __construct($input)
|
||||||
|
{
|
||||||
|
$this->input = $input;
|
||||||
|
Loop\addReadStream($this->input, function() {
|
||||||
|
while (($c = fgetc($this->input)) !== false) {
|
||||||
|
$this->buffer .= $c;
|
||||||
|
switch ($this->parsingMode) {
|
||||||
|
case ParsingMode::HEADERS:
|
||||||
|
if ($this->buffer === "\r\n") {
|
||||||
|
$this->parsingMode = ParsingMode::BODY;
|
||||||
|
$this->contentLength = (int)$this->headers['Content-Length'];
|
||||||
|
$this->buffer = '';
|
||||||
|
} else if (substr($this->buffer, -2) === "\r\n") {
|
||||||
|
$parts = explode(':', $this->buffer);
|
||||||
|
$this->headers[$parts[0]] = trim($parts[1]);
|
||||||
|
$this->buffer = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ParsingMode::BODY:
|
||||||
|
if (strlen($this->buffer) === $this->contentLength) {
|
||||||
|
if (isset($this->listener)) {
|
||||||
|
$msg = new Message(MessageBody::parse($this->buffer), $this->headers);
|
||||||
|
$listener = $this->listener;
|
||||||
|
$listener($msg);
|
||||||
|
}
|
||||||
|
$this->parsingMode = ParsingMode::HEADERS;
|
||||||
|
$this->headers = [];
|
||||||
|
$this->buffer = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable $listener Is called with a Message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onMessage(callable $listener)
|
||||||
|
{
|
||||||
|
$this->listener = $listener;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
class ProtocolStreamWriter implements ProtocolWriter
|
||||||
|
{
|
||||||
|
private $output;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $output
|
||||||
|
*/
|
||||||
|
public function __construct($output)
|
||||||
|
{
|
||||||
|
$this->output = $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a Message to the client
|
||||||
|
*
|
||||||
|
* @param Message $msg
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function write(Message $msg)
|
||||||
|
{
|
||||||
|
fwrite($this->output, (string)$msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use LanguageServer\Protocol\Message;
|
||||||
|
|
||||||
|
interface ProtocolWriter
|
||||||
|
{
|
||||||
|
public function write(Message $msg);
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\{NodeVisitorAbstract, Node};
|
||||||
|
|
||||||
|
class SymbolFinder extends NodeVisitorAbstract
|
||||||
|
{
|
||||||
|
const NODE_SYMBOL_KIND_MAP = [
|
||||||
|
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
||||||
|
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
||||||
|
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
||||||
|
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
||||||
|
Node\Stmt\ClassMethod::class => SymbolKind::METHOD,
|
||||||
|
Node\Stmt\PropertyProperty::class => SymbolKind::PROPERTY,
|
||||||
|
Node\Const_::class => SymbolKind::CONSTANT,
|
||||||
|
Node\Expr\Variable::class => SymbolKind::VARIABLE
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LanguageServer\Protocol\SymbolInformation[]
|
||||||
|
*/
|
||||||
|
public $symbols;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $containerName;
|
||||||
|
|
||||||
|
public function __construct(string $uri)
|
||||||
|
{
|
||||||
|
$this->uri = $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterNode(Node $node)
|
||||||
|
{
|
||||||
|
$class = get_class($node);
|
||||||
|
if (!isset(self::NODE_SYMBOL_MAP[$class])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$symbol = new SymbolInformation();
|
||||||
|
$symbol->kind = self::NODE_SYMBOL_MAP[$class];
|
||||||
|
$symbol->name = (string)$node->name;
|
||||||
|
$symbol->location = new Location(
|
||||||
|
$this->uri,
|
||||||
|
new Range(
|
||||||
|
new Position($node->getAttribute('startLine'), $node->getAttribute('startColumn')),
|
||||||
|
new Position($node->getAttribute('endLine'), $node->getAttribute('endColumn'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$symbol->containerName = $this->containerName;
|
||||||
|
$this->containerName = $symbol->name;
|
||||||
|
$this->symbols[] = $symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaveNode(Node $node)
|
||||||
|
{
|
||||||
|
$this->containerName = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,92 @@
|
||||||
|
|
||||||
namespace LanguageServer;
|
namespace LanguageServer;
|
||||||
|
|
||||||
|
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||||
|
use PhpParser\NodeVisitor\NameResolver;
|
||||||
|
use LanguageServer\Protocol\TextDocumentItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides method handlers for all textDocument/* methods
|
* Provides method handlers for all textDocument/* methods
|
||||||
*/
|
*/
|
||||||
class TextDocumentManager
|
class TextDocumentManager
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var PhpParser\Parser
|
||||||
|
*/
|
||||||
|
private $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map from file URIs to ASTs
|
||||||
|
*
|
||||||
|
* @var PhpParser\Stmt[][]
|
||||||
|
*/
|
||||||
|
private $asts;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]);
|
||||||
|
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The document symbol request is sent from the client to the server to list all symbols found in a given text
|
* The document symbol request is sent from the client to the server to list all symbols found in a given text
|
||||||
* document.
|
* document.
|
||||||
*
|
*
|
||||||
* @param LanguageServer\Protocol\Methods\TextDocument\DocumentSymbolParams $params
|
* @param LanguageServer\Protocol\TextDocumentIdentifier $textDocument
|
||||||
* @return SymbolInformation[]
|
* @return SymbolInformation[]
|
||||||
*/
|
*/
|
||||||
public function documentSymbol(DocumentSymbolParams $params): array
|
public function documentSymbol(TextDocumentIdentifier $textDocument): array
|
||||||
{
|
{
|
||||||
|
$stmts = $this->asts[$textDocument->uri];
|
||||||
|
if (!$stmts) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$finder = new SymbolFinder($textDocument->uri);
|
||||||
|
$traverser = new NodeTraverser;
|
||||||
|
$traverser->addVisitor($finder);
|
||||||
|
$traverser->traverse($stmts);
|
||||||
|
return $finder->symbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The document open notification is sent from the client to the server to signal newly opened text documents. The
|
||||||
|
* document's truth is now managed by the client and the server must not try to read the document's truth using the
|
||||||
|
* document's uri.
|
||||||
|
*
|
||||||
|
* @param LanguageServer\Protocol\TextDocumentItem $textDocument The document that was opened.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function didOpen(TextDocumentItem $textDocument)
|
||||||
|
{
|
||||||
|
$this->updateAst($textDocument->uri, $textDocument->text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The document change notification is sent from the client to the server to signal changes to a text document.
|
||||||
|
*
|
||||||
|
* @param LanguageServer\Protocol\VersionedTextDocumentIdentifier $textDocument
|
||||||
|
* @param LanguageServer\Protocol\TextDocumentContentChangeEvent[] $contentChanges
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges)
|
||||||
|
{
|
||||||
|
$this->updateAst($textDocument->uri, $contentChanges->text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateAst(string $uri, string $content)
|
||||||
|
{
|
||||||
|
$stmts = $parser->parse($content);
|
||||||
|
// TODO report errors as diagnostics
|
||||||
|
// foreach ($parser->getErrors() as $error) {
|
||||||
|
// error_log($error->getMessage());
|
||||||
|
// }
|
||||||
|
// $stmts can be null in case of a fatal parsing error
|
||||||
|
if ($stmts) {
|
||||||
|
$traverser = new NodeTraverser;
|
||||||
|
$traverser->addVisitor(new NameResolver);
|
||||||
|
$traverser->addVisitor(new ColumnCalculator($textDocument->text));
|
||||||
|
$traverser->traverse($stmts);
|
||||||
|
}
|
||||||
|
$this->asts[$uri] = $stmts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\LanguageServer;
|
||||||
|
use LanguageServer\Protocol\{Message, ClientCapabilities, TextDocumentSyncKind};
|
||||||
|
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
||||||
|
|
||||||
|
class LanguageServerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testInitialize()
|
||||||
|
{
|
||||||
|
$reader = new MockProtocolStream();
|
||||||
|
$writer = new MockProtocolStream();
|
||||||
|
$server = new LanguageServer($reader, $writer);
|
||||||
|
$msg = null;
|
||||||
|
$writer->onMessage(function (Message $message) use (&$msg) {
|
||||||
|
$msg = $message;
|
||||||
|
});
|
||||||
|
$reader->write(new Message(new RequestBody(1, 'initialize', [
|
||||||
|
'rootPath' => __DIR__,
|
||||||
|
'processId' => getmypid(),
|
||||||
|
'capabilities' => new ClientCapabilities()
|
||||||
|
])));
|
||||||
|
$this->assertNotNull($msg, 'onMessage callback should be called');
|
||||||
|
$this->assertInstanceOf(ResponseBody::class, $msg->body);
|
||||||
|
$this->assertNull($msg->body->error);
|
||||||
|
$this->assertEquals((object)[
|
||||||
|
'capabilities' => (object)[
|
||||||
|
'textDocumentSync' => TextDocumentSyncKind::FULL,
|
||||||
|
'documentSymbolProvider' => true,
|
||||||
|
'hoverProvider' => null,
|
||||||
|
'completionProvider' => null,
|
||||||
|
'signatureHelpProvider' => null,
|
||||||
|
'definitionProvider' => null,
|
||||||
|
'referencesProvider' => null,
|
||||||
|
'documentHighlightProvider' => null,
|
||||||
|
'workspaceSymbolProvider' => null,
|
||||||
|
'codeActionProvider' => null,
|
||||||
|
'codeLensProvider' => null,
|
||||||
|
'documentFormattingProvider' => null,
|
||||||
|
'documentRangeFormattingProvider' => null,
|
||||||
|
'documentOnTypeFormattingProvider' => null,
|
||||||
|
'renameProvider' => null
|
||||||
|
]
|
||||||
|
], $msg->body->result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests;
|
||||||
|
|
||||||
|
use LanguageServer\{ProtocolReader, ProtocolWriter};
|
||||||
|
use LanguageServer\Protocol\Message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake duplex protocol stream
|
||||||
|
*/
|
||||||
|
class MockProtocolStream implements ProtocolReader, ProtocolWriter
|
||||||
|
{
|
||||||
|
private $listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a Message to the client
|
||||||
|
*
|
||||||
|
* @param Message $msg
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function write(Message $msg)
|
||||||
|
{
|
||||||
|
if (isset($this->listener)) {
|
||||||
|
$listener = $this->listener;
|
||||||
|
$listener(Message::parse((string)$msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable $listener Is called with a Message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onMessage(callable $listener)
|
||||||
|
{
|
||||||
|
$this->listener = $listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace LanguageServer\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use LanguageServer\LanguageServer;
|
||||||
|
use LanguageServer\ProtocolStreamReader;
|
||||||
|
use LanguageServer\Protocol\Message;
|
||||||
|
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
||||||
|
use Sabre\Event\Loop;
|
||||||
|
|
||||||
|
class ProtocolStreamReaderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testParsingWorksAndListenerIsCalled()
|
||||||
|
{
|
||||||
|
$tmpfile = tempnam('', '');
|
||||||
|
$writeHandle = fopen($tmpfile, 'w');
|
||||||
|
$reader = new ProtocolStreamReader(fopen($tmpfile, 'r'));
|
||||||
|
$msg = null;
|
||||||
|
$reader->onMessage(function (Message $message) use (&$msg) {
|
||||||
|
$msg = $message;
|
||||||
|
});
|
||||||
|
$ret = fwrite($writeHandle, (string)new Message(new RequestBody(1, 'aMethod', ['arg' => 'Hello World'])));
|
||||||
|
Loop\tick();
|
||||||
|
$this->assertNotNull($msg);
|
||||||
|
$this->assertInstanceOf(Message::class, $msg);
|
||||||
|
$this->assertInstanceOf(RequestBody::class, $msg->body);
|
||||||
|
$this->assertEquals(1, $msg->body->id);
|
||||||
|
$this->assertEquals('aMethod', $msg->body->method);
|
||||||
|
$this->assertEquals((object)['arg' => 'Hello World'], $msg->body->params);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue