From 35a296b7f3c97e318dad9ee1773e77348687819f Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sun, 6 Nov 2016 14:47:36 +0100 Subject: [PATCH] Make it work --- composer.json | 4 +- src/Client/TextDocument.php | 26 +++++++- src/LanguageClient.php | 2 +- src/LanguageServer.php | 59 +++++++------------ src/Project.php | 22 +++++-- src/Protocol/ClientCapabilities.php | 9 +++ src/Protocol/TextDocumentContentResult.php | 11 ++++ src/utils.php | 30 ++++++++++ tests/NodeVisitor/DefinitionCollectorTest.php | 5 +- tests/PhpDocumentTest.php | 5 +- tests/ProjectTest.php | 12 +++- tests/Server/ServerTestCase.php | 4 +- .../Definition/GlobalFallbackTest.php | 4 +- tests/Server/TextDocument/DidChangeTest.php | 5 +- tests/Server/TextDocument/DidCloseTest.php | 4 +- tests/Server/TextDocument/FormattingTest.php | 6 +- tests/Server/TextDocument/ParseErrorsTest.php | 7 ++- .../References/GlobalFallbackTest.php | 4 +- 18 files changed, 152 insertions(+), 67 deletions(-) create mode 100644 src/Protocol/TextDocumentContentResult.php diff --git a/composer.json b/composer.json index ffb4776..62a9217 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,9 @@ "felixfbecker/advanced-json-rpc": "^2.0", "squizlabs/php_codesniffer" : "^2.7", "symfony/debug": "^3.1", - "netresearch/jsonmapper": "^1.0" + "netresearch/jsonmapper": "^1.0", + "webmozart/path-util": "^2.3", + "webmozart/glob": "^4.1" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Client/TextDocument.php b/src/Client/TextDocument.php index d5314e4..d96f7c1 100644 --- a/src/Client/TextDocument.php +++ b/src/Client/TextDocument.php @@ -6,6 +6,7 @@ namespace LanguageServer\Client; use LanguageServer\ClientHandler; use LanguageServer\Protocol\Message; use Sabre\Event\Promise; +use JsonMapper; /** * Provides method handlers for all textDocument/* methods @@ -17,9 +18,15 @@ class TextDocument */ private $handler; - public function __construct(ClientHandler $handler) + /** + * @var JsonMapper + */ + private $mapper; + + public function __construct(ClientHandler $handler, JsonMapper $mapper) { $this->handler = $handler; + $this->mapper = $mapper; } /** @@ -36,4 +43,21 @@ class TextDocument 'diagnostics' => $diagnostics ]); } + + /** + * The content request is sent from a server to a client + * to request the current content of a text document identified by the URI + * + * @param TextDocumentIdentifier $textDocument The document to get the content for + * @return Promise The document's current content + */ + public function xcontent(TextDocumentIdentifier $textDocument): Promise + { + return $this->handler->request( + 'textDocument/xcontent', + ['textDocument' => $textDocument] + )->then(function ($result) { + return $this->mapper->map($result, new TextDocumentContentResult); + }); + } } diff --git a/src/LanguageClient.php b/src/LanguageClient.php index 4745a4d..d21a9aa 100644 --- a/src/LanguageClient.php +++ b/src/LanguageClient.php @@ -33,7 +33,7 @@ class LanguageClient $handler = new ClientHandler($reader, $writer); $mapper = new JsonMapper; - $this->textDocument = new Client\TextDocument($handler); + $this->textDocument = new Client\TextDocument($handler, $mapper); $this->window = new Client\Window($handler); $this->workspace = new Client\Workspace($handler, $mapper); } diff --git a/src/LanguageServer.php b/src/LanguageServer.php index ce74c0b..014ae34 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -10,16 +10,18 @@ use LanguageServer\Protocol\{ Message, MessageType, InitializeResult, - SymbolInformation + SymbolInformation, + TextDocumentIdentifier }; use AdvancedJsonRpc; -use Sabre\Event\Loop; +use Sabre\Event\{Loop, Promise}; use function Sabre\Event\coroutine; use Exception; +use RuntimeException; use Throwable; use Generator; use Webmozart\Glob\Iterator\GlobIterator; -use Webmozart\PathUril\Path; +use Webmozart\PathUtil\Path; class LanguageServer extends AdvancedJsonRpc\Dispatcher { @@ -96,20 +98,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } $this->protocolWriter->write(new Message($responseBody)); } - })->otherwise(function ($err) { - // Crash process - Loop\nextTick(function () use ($err) { - throw $err; - }); - }); + })->otherwise('\\LanguageServer\\crash'); }); $this->protocolWriter = $writer; $this->client = new LanguageClient($reader, $writer); - - $this->project = new Project($this->client); - - $this->textDocument = new Server\TextDocument($this->project, $this->client); - $this->workspace = new Server\Workspace($this->project, $this->client); } /** @@ -125,15 +117,13 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $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(function ($err) { - // Crash process - Loop\nextTick(function () use ($err) { - throw $err; - }); - }); + $this->indexProject()->otherwise('\\LanguageServer\\crash'); } $serverCapabilities = new ServerCapabilities(); @@ -164,6 +154,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher */ public function shutdown() { + unset($this->project); } /** @@ -185,16 +176,16 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher { return coroutine(function () { $textDocuments = yield $this->globWorkspace('**/*.php'); - $count = count($uris); + $count = count($textDocuments); $startTime = microtime(true); foreach ($textDocuments as $i => $textDocument) { // Give LS to the chance to handle requests while indexing - Loop\tick(); + yield timeout(); $this->client->window->logMessage(MessageType::INFO, "Parsing file $i/$count: {$textDocument->uri}"); try { - $this->project->loadDocument($uri); + $this->project->loadDocument($textDocument->uri); } catch (Exception $e) { $this->client->window->logMessage(MessageType::ERROR, "Error parsing file $shortName: " . (string)$e); } @@ -220,21 +211,15 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher return $this->client->workspace->xglob($pattern); } else { // Use the file system - $promise = new Promise; - $textDocuments = []; - $pattern = Path::makeAbsolute($pattern, $this->rootPath); - $iterator = new GlobIterator($pattern); - $next = function () use ($iterator, &$textDocuments, $promise, &$next) { - if (!$iterator->valid()) { - $promise->resolve($textDocuments); - return; + 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(); } - $textDocuments[] = new TextDocumentIdentifier(pathToUri($iterator->current())); - $iterator->next(); - Loop\setTimeout($next, 0); - }; - Loop\setTimeout($next, 0); - return $promise; + return $textDocuments; + }); } } } diff --git a/src/Project.php b/src/Project.php index 0a5349f..361f7b0 100644 --- a/src/Project.php +++ b/src/Project.php @@ -3,7 +3,7 @@ declare(strict_types = 1); namespace LanguageServer; -use LanguageServer\Protocol\SymbolInformation; +use LanguageServer\Protocol\{SymbolInformation, TextDocumentIdentifier, ClientCapabilities}; use phpDocumentor\Reflection\DocBlockFactory; class Project @@ -51,10 +51,17 @@ class Project */ private $client; - public function __construct(LanguageClient $client) + /** + * The client's capabilities + * + * @var ClientCapabilities + */ + private $clientCapabilities; + + public function __construct(LanguageClient $client, ClientCapabilities $clientCapabilities) { $this->client = $client; - + $this->clientCapabilities = $clientCapabilities; $this->parser = new Parser; $this->docBlockFactory = DocBlockFactory::createInstance(); } @@ -80,11 +87,16 @@ class Project * The document is NOT added to the list of open documents, but definitions are registered. * * @param string $uri - * @return LanguageServer\PhpDocument + * @return Promise */ public function loadDocument(string $uri) { - $content = file_get_contents(uriToPath($uri)); + if ($this->clientCapabilities->xcontentProvider) { + // TODO: make this whole method async instead of calling wait() + $content = $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))->wait()->text; + } else { + $content = file_get_contents(uriToPath($uri)); + } if (isset($this->documents[$uri])) { $document = $this->documents[$uri]; $document->updateContent($content); diff --git a/src/Protocol/ClientCapabilities.php b/src/Protocol/ClientCapabilities.php index 45ab02a..f72a681 100644 --- a/src/Protocol/ClientCapabilities.php +++ b/src/Protocol/ClientCapabilities.php @@ -5,7 +5,16 @@ namespace LanguageServer\Protocol; class ClientCapabilities { /** + * The client supports workspace/xglob requests + * * @var bool|null */ public $xglobProvider; + + /** + * The client supports textDocument/xcontent requests + * + * @var bool|null + */ + public $xcontentProvider; } diff --git a/src/Protocol/TextDocumentContentResult.php b/src/Protocol/TextDocumentContentResult.php new file mode 100644 index 0000000..758de55 --- /dev/null +++ b/src/Protocol/TextDocumentContentResult.php @@ -0,0 +1,11 @@ + + */ +function timeout($seconds = 0): Promise +{ + $promise = new Promise; + Loop\setTimeout([$promise, 'fulfill'], $seconds); + return $promise; +} diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 0a8fdb1..46741dd 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; use LanguageServer\{LanguageClient, Project, PhpDocument, Parser}; +use LanguageServer\Protocol\ClientCapabilities; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use function LanguageServer\pathToUri; @@ -16,7 +17,7 @@ class DefinitionCollectorTest extends TestCase public function testCollectsSymbols() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); $document = $project->loadDocument($uri); @@ -55,7 +56,7 @@ class DefinitionCollectorTest extends TestCase public function testDoesNotCollectReferences() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $parser = new Parser; $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $document = $project->loadDocument($uri); diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index fce5dc4..5cdc8c8 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{LanguageClient, Project}; use LanguageServer\NodeVisitor\NodeAtPositionFinder; -use LanguageServer\Protocol\{SymbolKind, Position}; +use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; use PhpParser\Node; class PhpDocumentTest extends TestCase @@ -19,7 +19,8 @@ class PhpDocumentTest extends TestCase public function setUp() { - $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream)); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); + $this->project = new Project($client, new ClientCapabilities); } public function testParsesVariableVariables() diff --git a/tests/ProjectTest.php b/tests/ProjectTest.php index dc030d2..b8721c5 100644 --- a/tests/ProjectTest.php +++ b/tests/ProjectTest.php @@ -6,7 +6,14 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; -use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolKind, DiagnosticSeverity, FormattingOptions}; +use LanguageServer\Protocol\{ + TextDocumentItem, + TextDocumentIdentifier, + SymbolKind, + DiagnosticSeverity, + FormattingOptions, + ClientCapabilities +}; use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use function LanguageServer\pathToUri; @@ -19,7 +26,8 @@ class ProjectTest extends TestCase public function setUp() { - $this->project = new Project(new LanguageClient(new MockProtocolStream, new MockProtocolStream)); + $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); + $this->project = new Project($client, new ClientCapabilities); } public function testGetDocumentLoadsDocument() diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 24095a3..10ef636 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{Position, Location, Range}; +use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use function LanguageServer\pathToUri; abstract class ServerTestCase extends TestCase @@ -43,7 +43,7 @@ abstract class ServerTestCase extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client); + $this->project = new Project($client, new ClientCapabilities); $this->textDocument = new Server\TextDocument($this->project, $client); $this->workspace = new Server\Workspace($this->project, $client); diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index ea62f70..915b3d9 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -6,14 +6,14 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location}; +use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; class GlobalFallbackTest extends ServerTestCase { public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 0ee62ef..1df0505 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -12,7 +12,8 @@ use LanguageServer\Protocol\{ VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Range, - Position + Position, + ClientCapabilities }; class DidChangeTest extends TestCase @@ -20,7 +21,7 @@ class DidChangeTest extends TestCase public function test() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $textDocument = new Server\TextDocument($project, $client); $phpDocument = $project->openDocument('whatever', "openDocument('whatever', 'hello world'); diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index ca6fc2c..bae4721 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions}; +use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions, ClientCapabilities}; use function LanguageServer\{pathToUri, uriToPath}; class FormattingTest extends TestCase @@ -19,14 +19,14 @@ class FormattingTest extends TestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $this->textDocument = new Server\TextDocument($project, $client); } public function testFormatting() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $textDocument = new Server\TextDocument($project, $client); $path = realpath(__DIR__ . '/../../../fixtures/format.php'); $uri = pathToUri($path); diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index 128d58f..2a02efe 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -6,8 +6,9 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; -use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity}; +use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use Sabre\Event\Promise; +use JsonMapper; class ParseErrorsTest extends TestCase { @@ -25,7 +26,7 @@ class ParseErrorsTest extends TestCase private $args; public function __construct(&$args) { - parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream)); + parent::__construct(new ClientHandler(new MockProtocolStream, new MockProtocolStream), new JsonMapper); $this->args = &$args; } public function publishDiagnostics(string $uri, array $diagnostics): Promise @@ -34,7 +35,7 @@ class ParseErrorsTest extends TestCase return Promise\resolve(null); } }; - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $this->textDocument = new Server\TextDocument($project, $client); } diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 6862d97..0cde34a 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -6,7 +6,7 @@ namespace LanguageServer\Tests\Server\TextDocument\References; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, LanguageClient, Project}; -use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range}; +use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Tests\Server\ServerTestCase; class GlobalFallbackTest extends ServerTestCase @@ -14,7 +14,7 @@ class GlobalFallbackTest extends ServerTestCase public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client); + $project = new Project($client, new ClientCapabilities); $this->textDocument = new Server\TextDocument($project, $client); $project->openDocument('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); $project->openDocument('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php'));