From 41ad025fe7d462e1e0f1c6cda33929683b242b71 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 25 Aug 2016 15:27:14 +0200 Subject: [PATCH] Update --- README.md | 6 +- bin/main.php | 5 +- composer.json | 9 +- src/ColumnCalculator.php | 40 +++++ src/LanguageServer.php | 93 ++++++++-- ...Capabilites.php => ClientCapabilities.php} | 2 +- src/Protocol/InitializeResult.php | 21 +++ src/Protocol/Location.php | 6 + src/Protocol/Message.php | 79 +++++---- src/Protocol/Methods/InitializeResult.php | 24 --- src/Protocol/Position.php | 6 + src/Protocol/ProtocolServer.php | 165 ------------------ src/Protocol/Range.php | 6 + src/Protocol/Request.php | 25 --- src/Protocol/Response.php | 32 ---- src/Protocol/ResponseError.php | 36 ---- src/Protocol/ServerCapabilities.php | 2 +- src/Protocol/SymbolKind.php | 2 +- src/ProtocolReader.php | 9 + src/ProtocolStreamReader.php | 72 ++++++++ src/ProtocolStreamWriter.php | 28 +++ src/ProtocolWriter.php | 11 ++ src/SymbolFinder.php | 66 +++++++ src/TextDocumentManager.php | 76 +++++++- tests/LanguageServerTest.php | 50 ++++++ tests/MockProtocolStream.php | 39 +++++ tests/ProtocolReadStreamTest.php | 33 ++++ 27 files changed, 599 insertions(+), 344 deletions(-) create mode 100644 src/ColumnCalculator.php rename src/Protocol/{ClientCapabilites.php => ClientCapabilities.php} (65%) create mode 100644 src/Protocol/InitializeResult.php delete mode 100644 src/Protocol/Methods/InitializeResult.php delete mode 100644 src/Protocol/ProtocolServer.php delete mode 100644 src/Protocol/Request.php delete mode 100644 src/Protocol/Response.php delete mode 100644 src/Protocol/ResponseError.php create mode 100644 src/ProtocolReader.php create mode 100644 src/ProtocolStreamReader.php create mode 100644 src/ProtocolStreamWriter.php create mode 100644 src/ProtocolWriter.php create mode 100644 src/SymbolFinder.php create mode 100644 tests/LanguageServerTest.php create mode 100644 tests/MockProtocolStream.php create mode 100644 tests/ProtocolReadStreamTest.php diff --git a/README.md b/README.md index 8544ca8..3ec9129 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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). diff --git a/bin/main.php b/bin/main.php index 9cb8ea5..3dfd972 100644 --- a/bin/main.php +++ b/bin/main.php @@ -5,5 +5,6 @@ use Sabre\Event\Loop; require __DIR__ . '../vendor/autoload.php'; -$server = new LanguageServer(STDIN, STDOUT); -$server->run(); +$server = new LanguageServer(new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT)); + +Loop\run(); diff --git a/composer.json b/composer.json index 526d0a4..de42866 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "language-server", - "version": "0.0.1", + "name": "felixfbecker/language-server", + "version": "1.0.0", "description": "PHP Implementation of the Visual Studio Code Language Server Protocol", "authors": [ { @@ -23,14 +23,15 @@ "refactor" ], "bin": ["bin/main.php"], - "minimum-stability": "dev", "require": { "php": ">=7.0", "nikic/php-parser": "3.0.0alpha1", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^3.0", - "netresearch/jsonmapper": "^0.11.0" + "felixfbecker/advanced-json-rpc": "^1.2" }, + "minimum-stability": "dev", + "prefer-stable": true, "autoload": { "psr-4": { "LanguageServer\\": "src/" diff --git a/src/ColumnCalculator.php b/src/ColumnCalculator.php new file mode 100644 index 0000000..87dddcf --- /dev/null +++ b/src/ColumnCalculator.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/src/LanguageServer.php b/src/LanguageServer.php index bdcaff0..39eaeed 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -2,32 +2,91 @@ namespace LanguageServer; -use LanguageServer\Protocol\{ProtocolServer, ServerCapabilities, TextDocumentSyncKind}; -use LanguageServer\Protocol\Methods\{InitializeParams, InitializeResult}; +use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message}; +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; - protected $telemetry; - protected $window; - protected $workspace; - protected $completionItem; - protected $codeLens; + public $textDocument; + public $telemetry; + public $window; + public $workspace; + public $completionItem; + 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(); } - 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) - $capabilities->textDocumentSync = TextDocumentSyncKind::FULL; + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; // Support "Find all symbols" - $capabilities->documentSymbolProvider = true; - $result = new InitializeResult($capabilities); - return $result; + $serverCapabilities->documentSymbolProvider = true; + 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() + { + + } + + /** + * A notification to ask the server to exit its process. + * + * @return void + */ + public function exit() + { + exit(0); } } diff --git a/src/Protocol/ClientCapabilites.php b/src/Protocol/ClientCapabilities.php similarity index 65% rename from src/Protocol/ClientCapabilites.php rename to src/Protocol/ClientCapabilities.php index 746791d..c85e44b 100644 --- a/src/Protocol/ClientCapabilites.php +++ b/src/Protocol/ClientCapabilities.php @@ -2,7 +2,7 @@ namespace LanguageServer\Protocol; -class ClientCapabilites +class ClientCapabilities { } diff --git a/src/Protocol/InitializeResult.php b/src/Protocol/InitializeResult.php new file mode 100644 index 0000000..4a18e82 --- /dev/null +++ b/src/Protocol/InitializeResult.php @@ -0,0 +1,21 @@ +capabilities = $capabilities ?? new ServerCapabilities(); + } +} diff --git a/src/Protocol/Location.php b/src/Protocol/Location.php index 9766496..d6bdc14 100644 --- a/src/Protocol/Location.php +++ b/src/Protocol/Location.php @@ -16,4 +16,10 @@ class Location * @var Range */ public $range; + + public function __construct(string $uri = null, Range $range = null) + { + $this->uri = $uri; + $this->range = $range; + } } diff --git a/src/Protocol/Message.php b/src/Protocol/Message.php index da5ad05..8df5d0e 100644 --- a/src/Protocol/Message.php +++ b/src/Protocol/Message.php @@ -1,49 +1,64 @@ LanguageServer\Protocol\Methods\TextDocument\DidOpenNotification - $class = __NAMESPACE__ . '\\Methods\\' . implode('\\', array_map('ucfirst', explode('/', $decoded->method))) . (isset($decoded->id) ? 'Request' : 'Notification'); - - // If the Request/Notification type is unknown, instantiate a basic Request or Notification class - // (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; + $obj = new self; + $parts = explode("\r\n", $msg); + $obj->body = MessageBody::parse(array_pop($parts)); + foreach ($parts as $line) { + if ($line) { + $pair = explode(': ', $line); + $obj->headers[$pair[0]] = $pair[1]; } } + return $obj; + } - // JsonMapper will take care of recursively using the right classes for $params etc. - $mapper = new JsonMapper(); - $message = $mapper->map($decoded, new $class); + /** + * @param \AdvancedJsonRpc\Message $body + * @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; } } diff --git a/src/Protocol/Methods/InitializeResult.php b/src/Protocol/Methods/InitializeResult.php deleted file mode 100644 index 667ddc0..0000000 --- a/src/Protocol/Methods/InitializeResult.php +++ /dev/null @@ -1,24 +0,0 @@ -capabilities = $capabilites ?? new ServerCapabilities(); - } -} diff --git a/src/Protocol/Position.php b/src/Protocol/Position.php index 026d1fa..7831268 100644 --- a/src/Protocol/Position.php +++ b/src/Protocol/Position.php @@ -20,4 +20,10 @@ class Position * @var int */ public $character; + + public function __construct(int $line = null, int $character = null) + { + $this->line = $line; + $this->character = $character; + } } diff --git a/src/Protocol/ProtocolServer.php b/src/Protocol/ProtocolServer.php deleted file mode 100644 index 2aea53a..0000000 --- a/src/Protocol/ProtocolServer.php +++ /dev/null @@ -1,165 +0,0 @@ -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); - } -} diff --git a/src/Protocol/Range.php b/src/Protocol/Range.php index 0a562e0..c4a59ff 100644 --- a/src/Protocol/Range.php +++ b/src/Protocol/Range.php @@ -20,4 +20,10 @@ class Range * @var Position */ public $end; + + public function __construct(Position $start = null, Position $end = null) + { + $this->start = $start; + $this->end = $end; + } } diff --git a/src/Protocol/Request.php b/src/Protocol/Request.php deleted file mode 100644 index 3f6ff25..0000000 --- a/src/Protocol/Request.php +++ /dev/null @@ -1,25 +0,0 @@ -result = $result; - $this->error = $error; - } -} diff --git a/src/Protocol/ResponseError.php b/src/Protocol/ResponseError.php deleted file mode 100644 index 25e0950..0000000 --- a/src/Protocol/ResponseError.php +++ /dev/null @@ -1,36 +0,0 @@ -data = $data; - } -} diff --git a/src/Protocol/ServerCapabilities.php b/src/Protocol/ServerCapabilities.php index 4b800b0..6b3e4de 100644 --- a/src/Protocol/ServerCapabilities.php +++ b/src/Protocol/ServerCapabilities.php @@ -2,7 +2,7 @@ namespace LanguageServer\Protocol; -class ServerCapabilites +class ServerCapabilities { /** * Defines how text documents are synced. diff --git a/src/Protocol/SymbolKind.php b/src/Protocol/SymbolKind.php index 2603bb0..e8a44e2 100644 --- a/src/Protocol/SymbolKind.php +++ b/src/Protocol/SymbolKind.php @@ -11,7 +11,7 @@ abstract class SymbolKind const MODULE = 2; const NAMESPACE = 3; const PACKAGE = 4; - const _CLASS = 5; + const CLASS_ = 5; const METHOD = 6; const PROPERTY = 7; const FIELD = 8; diff --git a/src/ProtocolReader.php b/src/ProtocolReader.php new file mode 100644 index 0000000..1370504 --- /dev/null +++ b/src/ProtocolReader.php @@ -0,0 +1,9 @@ +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; + } +} diff --git a/src/ProtocolStreamWriter.php b/src/ProtocolStreamWriter.php new file mode 100644 index 0000000..0bb2ca4 --- /dev/null +++ b/src/ProtocolStreamWriter.php @@ -0,0 +1,28 @@ +output = $output; + } + + /** + * Sends a Message to the client + * + * @param Message $msg + * @return void + */ + private function write(Message $msg) + { + fwrite($this->output, (string)$msg); + } +} diff --git a/src/ProtocolWriter.php b/src/ProtocolWriter.php new file mode 100644 index 0000000..4055ddb --- /dev/null +++ b/src/ProtocolWriter.php @@ -0,0 +1,11 @@ + 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; + } +} diff --git a/src/TextDocumentManager.php b/src/TextDocumentManager.php index b034046..b7a09f8 100644 --- a/src/TextDocumentManager.php +++ b/src/TextDocumentManager.php @@ -2,20 +2,92 @@ 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 */ 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 * document. * - * @param LanguageServer\Protocol\Methods\TextDocument\DocumentSymbolParams $params + * @param LanguageServer\Protocol\TextDocumentIdentifier $textDocument * @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; } } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php new file mode 100644 index 0000000..4c1ca82 --- /dev/null +++ b/tests/LanguageServerTest.php @@ -0,0 +1,50 @@ +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); + } +} diff --git a/tests/MockProtocolStream.php b/tests/MockProtocolStream.php new file mode 100644 index 0000000..ce2c361 --- /dev/null +++ b/tests/MockProtocolStream.php @@ -0,0 +1,39 @@ +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; + } +} + diff --git a/tests/ProtocolReadStreamTest.php b/tests/ProtocolReadStreamTest.php new file mode 100644 index 0000000..5de0a7c --- /dev/null +++ b/tests/ProtocolReadStreamTest.php @@ -0,0 +1,33 @@ +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); + } +}