Skip to content

Enable LS to operate without accessing the file system #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Nov 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6d1f1ff
Add workspace/xGlob method
felixfbecker Oct 30, 2016
f2925a2
Use workspace/xGlob for indexing
felixfbecker Oct 30, 2016
b554ec5
Fixes
felixfbecker Nov 1, 2016
8116305
Rename xGlob to _glob
felixfbecker Nov 3, 2016
0060045
Make it work
felixfbecker Nov 3, 2016
b9aeea2
Add globWorkspace helper
felixfbecker Nov 3, 2016
35a296b
Make it work
felixfbecker Nov 6, 2016
381fd4d
Fix workspace/xglob method naming
felixfbecker Nov 6, 2016
9a13d64
Remove Generator return value support
felixfbecker Nov 6, 2016
1080d63
Remove wait(), async everything
felixfbecker Nov 6, 2016
fdb3d4b
Revert change in logging
felixfbecker Nov 6, 2016
6f6f6f3
Remove unneded line
felixfbecker Nov 6, 2016
545e1fd
Improve docblock
felixfbecker Nov 6, 2016
cf432fc
Remove findFilesRecursive fixtures
felixfbecker Nov 6, 2016
2e43112
Merge branch 'master' into no-fs
felixfbecker Nov 6, 2016
bb97500
Remove use statement
felixfbecker Nov 6, 2016
232f5c3
Integrate latest proposal changes
felixfbecker Nov 6, 2016
d833196
Array only for xglob $patterns
felixfbecker Nov 6, 2016
4df4195
Fix lint errors
felixfbecker Nov 7, 2016
745fec4
Merge branch 'master' into no-fs
felixfbecker Nov 7, 2016
898349b
Add tests for xglob/xcontent mode
felixfbecker Nov 7, 2016
cdb2c46
Integrate latest protocol changes
felixfbecker Nov 12, 2016
d9adfea
Fixes
felixfbecker Nov 12, 2016
471b88f
Correct documentation
felixfbecker Nov 12, 2016
981cb75
Correct docblock return type
felixfbecker Nov 14, 2016
7efb2c9
Remove unused import
felixfbecker Nov 14, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
"sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0",
"squizlabs/php_codesniffer" : "^2.7",
"symfony/debug": "^3.1"
"symfony/debug": "^3.1",
"netresearch/jsonmapper": "^1.0",
"webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1",
"sabre/uri": "^2.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand Down
1 change: 0 additions & 1 deletion fixtures/recursive/a.txt

This file was deleted.

1 change: 0 additions & 1 deletion fixtures/recursive/search/b.txt

This file was deleted.

1 change: 0 additions & 1 deletion fixtures/recursive/search/here/c.txt

This file was deleted.

28 changes: 26 additions & 2 deletions src/Client/TextDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
namespace LanguageServer\Client;

use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use LanguageServer\Protocol\{Message, TextDocumentItem, TextDocumentIdentifier};
use Sabre\Event\Promise;
use JsonMapper;

/**
* Provides method handlers for all textDocument/* methods
Expand All @@ -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;
}

/**
Expand All @@ -36,4 +43,21 @@ public function publishDiagnostics(string $uri, array $diagnostics): Promise
'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 <TextDocumentItem> 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 TextDocumentItem);
});
}
}
47 changes: 47 additions & 0 deletions src/Client/Workspace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);

namespace LanguageServer\Client;

use LanguageServer\ClientHandler;
use LanguageServer\Protocol\TextDocumentIdentifier;
use Sabre\Event\Promise;
use JsonMapper;

/**
* Provides method handlers for all workspace/* methods
*/
class Workspace
{
/**
* @var ClientHandler
*/
private $handler;

/**
* @var JsonMapper
*/
private $mapper;

public function __construct(ClientHandler $handler, JsonMapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}

/**
* Returns a list of all files in a directory
*
* @param string $base The base directory (defaults to the workspace)
* @return Promise <TextDocumentIdentifier[]> Array of documents
*/
public function xfiles(string $base = null): Promise
{
return $this->handler->request(
'workspace/xfiles',
['base' => $base]
)->then(function (array $textDocuments) {
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
});
}
}
13 changes: 12 additions & 1 deletion src/LanguageClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace LanguageServer;

use JsonMapper;

class LanguageClient
{
/**
Expand All @@ -19,11 +21,20 @@ class LanguageClient
*/
public $window;

/**
* Handles workspace/* methods
*
* @var Client\Workspace
*/
public $workspace;

public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
{
$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);
}
}
171 changes: 109 additions & 62 deletions src/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@
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 Throwable;
use Webmozart\Glob\Iterator\GlobIterator;
use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path;
use Sabre\Uri;

class LanguageServer extends AdvancedJsonRpc\Dispatcher
{
Expand All @@ -38,6 +44,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
public $completionItem;
public $codeLens;

/**
* ClientCapabilities
*/
private $clientCapabilities;

private $protocolReader;
private $protocolWriter;
private $client;
Expand All @@ -55,40 +66,42 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
parent::__construct($this, '/');
$this->protocolReader = $reader;
$this->protocolReader->on('message', function (Message $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 = $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);
coroutine(function () use ($msg) {
// Ignore responses, this is the handler for requests and notifications
if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
return;
}
$this->protocolWriter->write(new Message($responseBody));
}
$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);

$this->project = new Project($this->client);

$this->textDocument = new Server\TextDocument($this->project, $this->client);
$this->workspace = new Server\Workspace($this->project, $this->client);
}

/**
Expand All @@ -102,10 +115,14 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
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();
$this->indexProject()->otherwise('\\LanguageServer\\crash');
}

$serverCapabilities = new ServerCapabilities();
Expand Down Expand Up @@ -136,6 +153,7 @@ public function initialize(int $processId, ClientCapabilities $capabilities, str
*/
public function shutdown()
{
unset($this->project);
}

/**
Expand All @@ -151,42 +169,71 @@ public function exit()
/**
* Parses workspace files, one at a time.
*
* @return void
* @return Promise <void>
*/
private function indexProject()
private function indexProject(): Promise
{
$fileList = findFilesRecursive($this->rootPath, '/^.+\.php$/i');
$numTotalFiles = count($fileList);

$startTime = microtime(true);
$fileNum = 0;

$processFile = function () use (&$fileList, &$fileNum, &$processFile, $numTotalFiles, $startTime) {
if ($fileNum < $numTotalFiles) {
$file = $fileList[$fileNum];
$uri = pathToUri($file);
$fileNum++;
$shortName = substr($file, strlen($this->rootPath) + 1);

if (filesize($file) > 500000) {
$this->client->window->logMessage(MessageType::INFO, "Not parsing $shortName because it exceeds size limit of 0.5MB");
} else {
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
return coroutine(function () {
$textDocuments = yield $this->findPhpFiles();
$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 {
$this->project->loadDocument($uri);
yield $this->project->loadDocument($textDocument->uri);
} catch (Exception $e) {
$this->client->window->logMessage(MessageType::ERROR, "Error parsing file $shortName: " . (string)$e);
$this->client->window->logMessage(
MessageType::ERROR,
"Error parsing file {$textDocument->uri}: " . (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 $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
});
}

Loop\setTimeout($processFile, 0);
/**
* Returns all PHP files in the workspace.
* If the client does not support workspace/files, it falls back to searching the file system directly.
*
* @return Promise <TextDocumentIdentifier[]>
*/
private function findPhpFiles(): Promise
{
return coroutine(function () {
$textDocuments = [];
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
if ($this->clientCapabilities->xfilesProvider) {
// Use xfiles request
foreach (yield $this->client->workspace->xfiles() as $textDocument) {
$path = Uri\parse($textDocument->uri)['path'];
if (Glob::match($path, $pattern)) {
$textDocuments[] = $textDocument;
}
}
} else {
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(MessageType::INFO, "All $numTotalFiles PHP files parsed in $duration seconds. $mem MiB allocated.");
// Use the file system
foreach (new GlobIterator($pattern) as $path) {
$textDocuments[] = new TextDocumentIdentifier(pathToUri($path));
yield timeout();
}
}
};

Loop\setTimeout($processFile, 0);
return $textDocuments;
});
}
}
Loading