1
0
Fork 0
pull/6/head
Felix Becker 2016-08-25 15:27:14 +02:00
parent 138b529df1
commit 41ad025fe7
27 changed files with 599 additions and 344 deletions

View File

@ -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).
[![Version](https://img.shields.io/packagist/v/felixfbecker/language-server.svg)]() [![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)]() [![Version](https://img.shields.io/packagist/v/felixfbecker/language-server.svg)](https://packagist.org/packages/felixfbecker/language-server)
[![Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server)
[![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://packagist.org/packages/felixfbecker/language-server)
A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).

View File

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

View File

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

40
src/ColumnCalculator.php Normal file
View File

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

View File

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

View File

@ -2,7 +2,7 @@
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
class ClientCapabilites class ClientCapabilities
{ {
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

9
src/ProtocolReader.php Normal file
View File

@ -0,0 +1,9 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
interface ProtocolReader
{
public function onMessage(callable $listener);
}

View File

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

View File

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

11
src/ProtocolWriter.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\Message;
interface ProtocolWriter
{
public function write(Message $msg);
}

66
src/SymbolFinder.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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