diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 31fc52d..bdcaff0 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -2,17 +2,32 @@ namespace LanguageServer; -use LanguageServer\Protocol\{ProtocolServer, ServerCapabilities}; -use LanguageServer\Protocol\Methods\Initialize\{InitializeRequest, InitializeResult, InitializeResponse}; +use LanguageServer\Protocol\{ProtocolServer, ServerCapabilities, TextDocumentSyncKind}; +use LanguageServer\Protocol\Methods\{InitializeParams, InitializeResult}; class LanguageServer extends ProtocolServer { - public function initialize(InitializeRequest $req): InitializeResponse + protected $textDocument; + protected $telemetry; + protected $window; + protected $workspace; + protected $completionItem; + protected $codeLens; + + public function __construct($input, $output) { - $result = new InitializeResult; - $result->capabilites = new ServerCapabilities; - return new InitializeResponse($result); + parent::__construct($input, $output); + $this->textDocument = new TextDocumentManager(); } - public function shutdown + protected function initialize(InitializeParams $req): InitializeResult + { + $capabilities = new ServerCapabilites(); + // Ask the client to return always full documents (because we need to rebuild the AST from scratch) + $capabilities->textDocumentSync = TextDocumentSyncKind::FULL; + // Support "Find all symbols" + $capabilities->documentSymbolProvider = true; + $result = new InitializeResult($capabilities); + return $result; + } } diff --git a/src/Protocol/Methods/$/CancelRequestNotification.php b/src/Protocol/Methods/$/CancelRequestNotification.php index 3c6a9b7..9e872dd 100644 --- a/src/Protocol/Methods/$/CancelRequestNotification.php +++ b/src/Protocol/Methods/$/CancelRequestNotification.php @@ -7,7 +7,7 @@ use LanguageServer\Protocol\Notification; class CancelRequestNotification extends Notification { /** - * @var CancelParams + * @var CancelRequestParams */ public $params; } diff --git a/src/Protocol/Methods/Initialize/InitializeError.php b/src/Protocol/Methods/InitializeError.php similarity index 100% rename from src/Protocol/Methods/Initialize/InitializeError.php rename to src/Protocol/Methods/InitializeError.php diff --git a/src/Protocol/Methods/Initialize/InitializeParams.php b/src/Protocol/Methods/InitializeParams.php similarity index 89% rename from src/Protocol/Methods/Initialize/InitializeParams.php rename to src/Protocol/Methods/InitializeParams.php index ea725b3..e1995e7 100644 --- a/src/Protocol/Methods/Initialize/InitializeParams.php +++ b/src/Protocol/Methods/InitializeParams.php @@ -1,6 +1,6 @@ capabilities = $capabilites ?? new ServerCapabilities(); + } } diff --git a/src/Protocol/ProtocolServer.php b/src/Protocol/ProtocolServer.php index 1435c5f..2aea53a 100644 --- a/src/Protocol/ProtocolServer.php +++ b/src/Protocol/ProtocolServer.php @@ -3,7 +3,7 @@ namespace LanguageServer\Protocol; use Sabre\Event\Loop; -use LanguageServer\Protocol\Methods\Initialize\{InitializeRequest, InitializeResponse}; +use LanguageServer\Protocol\Methods\{InitializeRequest, InitializeResponse}; abstract class ParsingMode { const HEADERS = 1; @@ -29,11 +29,16 @@ abstract class ProtocolServer $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->output); + $this->buffer .= fgetc($this->input); switch ($parsingMode) { case ParsingMode::HEADERS: if (substr($buffer, -4) === '\r\n\r\n') { @@ -48,18 +53,28 @@ abstract class ProtocolServer break; case ParsingMode::BODY: if (strlen($buffer) === $contentLength) { - $req = Message::parse($body, Request::class); - if (!method_exists($this, $req->method)) { - $this->sendResponse(new Response(null, new ResponseError("Method {$req->method} is not implemented", ErrorCode::METHOD_NOT_FOUND, $e))); - } else { - try { - $result = $this->{$req->method}($req->params); - $this->sendResponse(new Response($result)); - } catch (ResponseError $e) { - $this->sendResponse(new Response(null, $e)); - } catch (Throwable $e) { - $this->sendResponse(new Response(null, new ResponseError($e->getMessage(), $e->getCode(), null, $e))); - } + $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 = ''; @@ -71,10 +86,80 @@ abstract class ProtocolServer Loop\run(); } - public function sendResponse(Response $res) + /** + * Calls the appropiate method handler for an incoming Message + * + * @param Message $msg The incoming message + * @return Result|void + */ + private function dispatch(Message $msg) { - fwrite($this->output, json_encode($res)); + // 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); } - abstract public function initialize(InitializeRequest $req): InitializeResponse; + /** + * 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/Response.php b/src/Protocol/Response.php index db7f02d..9ba4368 100644 --- a/src/Protocol/Response.php +++ b/src/Protocol/Response.php @@ -24,7 +24,7 @@ class Response extends Message */ public $error; - public function __construct($result, ResponseError $error = null) + public function __construct($id, string $method, $result, ResponseError $error = null) { $this->result = $result; $this->error = $error; diff --git a/src/TextDocumentManager.php b/src/TextDocumentManager.php new file mode 100644 index 0000000..b034046 --- /dev/null +++ b/src/TextDocumentManager.php @@ -0,0 +1,21 @@ +