1
0
Fork 0

Make it work

pull/136/head
Felix Becker 2016-11-06 14:47:36 +01:00
parent b9aeea2523
commit 35a296b7f3
18 changed files with 152 additions and 67 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <LanguageServer\PhpDocument>
*/
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);

View File

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

View File

@ -0,0 +1,11 @@
<?php
namespace LanguageServer\Protocol;
class TextDocumentContentResult
{
/**
* @var string
*/
public $text;
}

View File

@ -3,7 +3,9 @@ declare(strict_types = 1);
namespace LanguageServer;
use Throwable;
use InvalidArgumentException;
use Sabre\Event\{Loop, Promise};
/**
* Transforms an absolute file path into a URI as used by the language server protocol.
@ -47,3 +49,31 @@ function uriToPath(string $uri)
}
return $filepath;
}
/**
* Throws an exception on the next tick.
* Useful for letting a promise crash the process on rejection.
*
* @param Throwable $err
* @return void
*/
function crash(Throwable $err)
{
Loop\nextTick(function () use ($err) {
throw $err;
});
}
/**
* Returns a promise that is resolved after x seconds.
* Useful for giving back control to the event loop inside a coroutine.
*
* @param int $seconds
* @return Promise <void>
*/
function timeout($seconds = 0): Promise
{
$promise = new Promise;
Loop\setTimeout([$promise, 'fulfill'], $seconds);
return $promise;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', "<?php\necho 'Hello, World'\n");

View File

@ -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\{TextDocumentItem, TextDocumentIdentifier};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
use Exception;
class DidCloseTest extends TestCase
@ -14,7 +14,7 @@ class DidCloseTest 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', 'hello world');

View File

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

View File

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

View File

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