protocolReader = $reader; $this->protocolReader->on('message', function (Message $msg) { coroutine(function () use ($msg) { // Ignore responses, this is the handler for requests and notifications if (AdvancedJsonRpc\Response::isResponse($msg->body)) { return; } $result = null; $error = null; try { // Invoke the method handler to get a result $result = yield $this->dispatch($msg->body); } catch (AdvancedJsonRpc\Error $e) { // If a ResponseError is thrown, send it back in the Response $error = $e; } catch (Throwable $e) { // If an unexpected error occured, send back an INTERNAL_ERROR error response $error = new AdvancedJsonRpc\Error( $e->getMessage(), AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e ); } // Only send a Response for a Request // Notifications do not send Responses if (AdvancedJsonRpc\Request::isRequest($msg->body)) { if ($error !== null) { $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); } else { $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); } $this->protocolWriter->write(new Message($responseBody)); } })->otherwise('\\LanguageServer\\crash'); }); $this->protocolWriter = $writer; $this->client = new LanguageClient($reader, $writer); } /** * The initialize request is sent as the first request from the client to the server. * * @param int $processId The process Id of the parent process that started the server. * @param ClientCapabilities $capabilities The capabilities provided by the client (editor) * @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open. * @return InitializeResult */ public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult { $this->rootPath = $rootPath; $this->clientCapabilities = $capabilities; $this->project = new Project($this->client, $capabilities); $this->textDocument = new Server\TextDocument($this->project, $this->client); $this->workspace = new Server\Workspace($this->project, $this->client); // start building project index if ($rootPath !== null) { $this->indexProject()->otherwise('\\LanguageServer\\crash'); } $serverCapabilities = new ServerCapabilities(); // Ask the client to return always full documents (because we need to rebuild the AST from scratch) $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; // Support "Find all symbols" $serverCapabilities->documentSymbolProvider = true; // Support "Find all symbols in workspace" $serverCapabilities->workspaceSymbolProvider = true; // Support "Format Code" $serverCapabilities->documentFormattingProvider = true; // Support "Go to definition" $serverCapabilities->definitionProvider = true; // Support "Find all references" $serverCapabilities->referencesProvider = true; // Support "Hover" $serverCapabilities->hoverProvider = 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() { unset($this->project); } /** * A notification to ask the server to exit its process. * * @return void */ public function exit() { exit(0); } /** * Parses workspace files, one at a time. * * @return Promise */ private function indexProject(): Promise { return coroutine(function () { $textDocuments = yield $this->globWorkspace(['**/*.php']); $count = count($textDocuments); $startTime = microtime(true); yield Promise\all(array_map(function ($textDocument, $i) use ($count) { return coroutine(function () use ($textDocument, $i, $count) { // Give LS to the chance to handle requests while indexing yield timeout(); $this->client->window->logMessage( MessageType::INFO, "Parsing file $i/$count: {$textDocument->uri}" ); try { yield $this->project->loadDocument($textDocument->uri); } catch (Exception $e) { $this->client->window->logMessage( MessageType::ERROR, "Error parsing file $shortName: " . (string)$e ); } }); }, $textDocuments, array_keys($textDocuments))); $duration = (int)(microtime(true) - $startTime); $mem = (int)(memory_get_usage(true) / (1024 * 1024)); $this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated."); }); } /** * Returns all files matching a glob pattern. * If the client does not support workspace/xglob, it falls back to globbing the file system directly. * * @param string $patterns * @return Promise */ private function globWorkspace(array $patterns): Promise { if ($this->clientCapabilities->xglobProvider) { // Use xglob request return $this->client->workspace->xglob($patterns); } else { // Use the file system $textDocuments = []; return Promise\all(array_map(function ($pattern) use (&$textDocuments) { return coroutine(function () use ($pattern, &$textDocuments) { $pattern = Path::makeAbsolute($pattern, $this->rootPath); foreach (new GlobIterator($pattern) as $path) { $textDocuments[] = new TextDocumentIdentifier(pathToUri($path)); yield timeout(); } }); }, $patterns))->then(function () use ($textDocuments) { return $textDocuments; }); } } }