Merge a81bed93c7
into b4b4a2fff5
commit
1be249cfdc
|
@ -0,0 +1 @@
|
|||
<?php
|
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\ConfigurationItem;
|
||||
use LanguageServer\Protocol\TextDocumentIdentifier;
|
||||
use Sabre\Event\Promise;
|
||||
use JsonMapper;
|
||||
|
@ -44,4 +45,24 @@ class Workspace
|
|||
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace/configuration request is sent from the server to the
|
||||
* client to fetch configuration settings from the client.
|
||||
*
|
||||
* The request can fetch n configuration settings in one roundtrip.
|
||||
* The order of the returned configuration settings correspond to the order
|
||||
* of the passed ConfigurationItems (e.g. the first item in the response is
|
||||
* the result for the first configuration item in the params).
|
||||
*
|
||||
* @param ConfigurationItem[] $items
|
||||
* @return Promise <mixed[]>
|
||||
*/
|
||||
public function configuration(array $items): Promise
|
||||
{
|
||||
return $this->handler->request(
|
||||
'workspace/configuration',
|
||||
['items' => $items]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,4 +147,14 @@ abstract class AbstractAggregateIndex implements ReadableIndex
|
|||
}
|
||||
return $refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe all indexes for a reindex
|
||||
*/
|
||||
public function wipe()
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
$index->wipe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,4 +222,15 @@ class Index implements ReadableIndex, \Serializable
|
|||
'staticComplete' => $this->staticComplete
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear indexed references and definitions
|
||||
*/
|
||||
public function wipe()
|
||||
{
|
||||
$this->definitions = [];
|
||||
$this->references = [];
|
||||
$this->complete = false;
|
||||
$this->staticComplete = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class ProjectIndex extends AbstractAggregateIndex
|
|||
/**
|
||||
* @return ReadableIndex[]
|
||||
*/
|
||||
protected function getIndexes(): array
|
||||
public function getIndexes(): array
|
||||
{
|
||||
return [$this->sourceIndex, $this->dependenciesIndex];
|
||||
}
|
||||
|
|
|
@ -53,6 +53,11 @@ class Indexer
|
|||
*/
|
||||
private $documentLoader;
|
||||
|
||||
/**
|
||||
* @var Options
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @var \stdClasss
|
||||
*/
|
||||
|
@ -63,6 +68,16 @@ class Indexer
|
|||
*/
|
||||
private $composerJson;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $hasCancellationSignal;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isIndexing;
|
||||
|
||||
/**
|
||||
* @param FilesFinder $filesFinder
|
||||
* @param string $rootPath
|
||||
|
@ -93,6 +108,22 @@ class Indexer
|
|||
$this->documentLoader = $documentLoader;
|
||||
$this->composerLock = $composerLock;
|
||||
$this->composerJson = $composerJson;
|
||||
$this->hasCancellationSignal = false;
|
||||
$this->isIndexing = false;
|
||||
$this->options = new Options();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Options $options
|
||||
*/
|
||||
public function setOptions(Options $options)
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function getOptions(): Options
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,13 +134,14 @@ class Indexer
|
|||
public function index(): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
|
||||
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
|
||||
$fileTypes = implode(',', $this->options->fileTypes);
|
||||
$pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath);
|
||||
$uris = yield $this->filesFinder->find($pattern);
|
||||
|
||||
$count = count($uris);
|
||||
$startTime = microtime(true);
|
||||
$this->client->window->logMessage(MessageType::INFO, "$count files total");
|
||||
$this->isIndexing = true;
|
||||
|
||||
/** @var string[] */
|
||||
$source = [];
|
||||
|
@ -135,6 +167,7 @@ class Indexer
|
|||
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
|
||||
yield $this->indexFiles($source);
|
||||
$this->sourceIndex->setStaticComplete();
|
||||
|
||||
// Dynamic references
|
||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
|
||||
yield $this->indexFiles($source);
|
||||
|
@ -187,6 +220,7 @@ class Indexer
|
|||
}
|
||||
}
|
||||
|
||||
$this->isIndexing = false;
|
||||
$duration = (int)(microtime(true) - $startTime);
|
||||
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
|
||||
$this->client->window->logMessage(
|
||||
|
@ -196,6 +230,35 @@ class Indexer
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current indexing state
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isIndexing(): bool
|
||||
{
|
||||
return $this->isIndexing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all running indexing processes
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function cancel(): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
$this->hasCancellationSignal = true;
|
||||
|
||||
while ($this->isIndexing()) {
|
||||
yield timeout();
|
||||
}
|
||||
|
||||
$this->hasCancellationSignal = false;
|
||||
$this->client->window->logMessage(MessageType::INFO, 'Indexing project canceled');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
* @return Promise
|
||||
|
@ -204,6 +267,11 @@ class Indexer
|
|||
{
|
||||
return coroutine(function () use ($files) {
|
||||
foreach ($files as $i => $uri) {
|
||||
// abort current running indexing
|
||||
if ($this->hasCancellationSignal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip open documents
|
||||
if ($this->documentLoader->isOpen($uri)) {
|
||||
continue;
|
||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\{
|
||||
ConfigurationItem,
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
TextDocumentSyncKind,
|
||||
|
@ -15,7 +16,7 @@ use LanguageServer\Protocol\{
|
|||
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
|
||||
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
|
||||
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
|
||||
use LanguageServer\Cache\{FileSystemCache, ClientCache};
|
||||
use LanguageServer\Cache\{Cache, FileSystemCache, ClientCache};
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
|
@ -106,6 +107,26 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
*/
|
||||
protected $definitionResolver;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $rootPath;
|
||||
|
||||
/**
|
||||
* @var Cache
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* @var ClientCapabilities
|
||||
*/
|
||||
protected $clientCapabilities;
|
||||
|
||||
/**
|
||||
* @var Indexer
|
||||
*/
|
||||
protected $indexer;
|
||||
|
||||
/**
|
||||
* @param ProtocolReader $reader
|
||||
* @param ProtocolWriter $writer
|
||||
|
@ -162,13 +183,18 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
*
|
||||
* @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.
|
||||
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
|
||||
* @param int|null $processId The process Id of the parent process that started the server.
|
||||
* Is null if the process has not been started by another process.
|
||||
* If the parent process is not alive then the server should exit
|
||||
* (see exit notification) its process.
|
||||
* @return Promise <InitializeResult>
|
||||
*/
|
||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise
|
||||
{
|
||||
public function initialize(
|
||||
ClientCapabilities $capabilities,
|
||||
string $rootPath = null,
|
||||
int $processId = null
|
||||
): Promise {
|
||||
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
||||
|
||||
if ($capabilities->xfilesProvider) {
|
||||
$this->filesFinder = new ClientFilesFinder($this->client);
|
||||
} else {
|
||||
|
@ -186,57 +212,64 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson);
|
||||
$stubsIndex = StubsIndex::read();
|
||||
$this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex);
|
||||
$this->rootPath = $rootPath;
|
||||
$this->clientCapabilities = $capabilities;
|
||||
|
||||
// The DefinitionResolver should look in stubs, the project source and dependencies
|
||||
$this->definitionResolver = new DefinitionResolver($this->globalIndex);
|
||||
|
||||
$this->documentLoader = new PhpDocumentLoader(
|
||||
$this->contentRetriever,
|
||||
$this->projectIndex,
|
||||
$this->definitionResolver
|
||||
);
|
||||
|
||||
if ($rootPath !== null) {
|
||||
yield $this->beforeIndex($rootPath);
|
||||
if ($this->rootPath !== null) {
|
||||
yield $this->beforeIndex($this->rootPath);
|
||||
|
||||
// Find composer.json
|
||||
if ($this->composerJson === null) {
|
||||
$composerJsonFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.json', $rootPath));
|
||||
$composerJsonFiles = yield $this->filesFinder->find(
|
||||
Path::makeAbsolute('**/composer.json', $this->rootPath)
|
||||
);
|
||||
sortUrisLevelOrder($composerJsonFiles);
|
||||
|
||||
if (!empty($composerJsonFiles)) {
|
||||
$this->composerJson = json_decode(yield $this->contentRetriever->retrieve($composerJsonFiles[0]));
|
||||
$this->composerJson = json_decode(
|
||||
yield $this->contentRetriever->retrieve($composerJsonFiles[0])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Find composer.lock
|
||||
if ($this->composerLock === null) {
|
||||
$composerLockFiles = yield $this->filesFinder->find(Path::makeAbsolute('**/composer.lock', $rootPath));
|
||||
$composerLockFiles = yield $this->filesFinder->find(
|
||||
Path::makeAbsolute('**/composer.lock', $this->rootPath)
|
||||
);
|
||||
sortUrisLevelOrder($composerLockFiles);
|
||||
|
||||
if (!empty($composerLockFiles)) {
|
||||
$this->composerLock = json_decode(yield $this->contentRetriever->retrieve($composerLockFiles[0]));
|
||||
$this->composerLock = json_decode(
|
||||
yield $this->contentRetriever->retrieve($composerLockFiles[0])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
|
||||
$this->cache = $capabilities->xcacheProvider ? new ClientCache($this->client) : new FileSystemCache;
|
||||
|
||||
// Index in background
|
||||
$indexer = new Indexer(
|
||||
$this->indexer = new Indexer(
|
||||
$this->filesFinder,
|
||||
$rootPath,
|
||||
$this->rootPath,
|
||||
$this->client,
|
||||
$cache,
|
||||
$this->cache,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$this->documentLoader,
|
||||
$this->composerLock,
|
||||
$this->composerJson
|
||||
);
|
||||
$indexer->index()->otherwise('\\LanguageServer\\crash');
|
||||
}
|
||||
|
||||
|
||||
if ($this->textDocument === null) {
|
||||
$this->textDocument = new Server\TextDocument(
|
||||
$this->documentLoader,
|
||||
|
@ -247,12 +280,14 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$this->composerLock
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->workspace === null) {
|
||||
$this->workspace = new Server\Workspace(
|
||||
$this->client,
|
||||
$this->projectIndex,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$this->indexer,
|
||||
$this->composerLock,
|
||||
$this->documentLoader,
|
||||
$this->composerJson
|
||||
|
@ -289,10 +324,46 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The initialized notification is sent from the client to the server after
|
||||
* the client received the result of the initialize request but before the
|
||||
* client is sending any other request or notification to the server.
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function initialized(): Promise
|
||||
{
|
||||
return coroutine(function () {
|
||||
if (!$this->rootPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// request configuration if it is supported
|
||||
// support comes with protocol version 3.6.0
|
||||
if ($this->clientCapabilities->workspace->configuration) {
|
||||
$configuration = yield $this->client->workspace->configuration([new ConfigurationItem('php')]);
|
||||
$options = $this->mapper->map($configuration[0], new Options());
|
||||
}
|
||||
|
||||
// depending on the implementation of the client
|
||||
// the workspace/didChangeConfiguration can be invoked before
|
||||
// the response from the workspace/configuration request is resolved
|
||||
if ($this->indexer->isIndexing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options) {
|
||||
$this->indexer->setOptions($options);
|
||||
}
|
||||
|
||||
$this->indexer->index()->otherwise('\\LanguageServer\\crash');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* (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
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
class Options
|
||||
{
|
||||
/**
|
||||
* File types the indexer should process
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $fileTypes = ['.php'];
|
||||
|
||||
/**
|
||||
* Validate/Filter input and set options for file types
|
||||
*
|
||||
* @param string[] $fileTypes List of file types
|
||||
*/
|
||||
public function setFileTypes(array $fileTypes)
|
||||
{
|
||||
$fileTypes = filter_var_array($fileTypes, FILTER_SANITIZE_STRING);
|
||||
$fileTypes = filter_var($fileTypes, FILTER_CALLBACK, ['options' => [$this, 'filterFileTypes']]);
|
||||
$fileTypes = array_filter($fileTypes, 'strlen');
|
||||
$fileTypes = array_values($fileTypes);
|
||||
|
||||
$this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter valid file type
|
||||
*
|
||||
* @param string $fileType The file type to filter
|
||||
* @return string|bool If valid it returns the file type, otherwise false
|
||||
*/
|
||||
private function filterFileTypes(string $fileType)
|
||||
{
|
||||
$fileType = trim($fileType);
|
||||
|
||||
if (empty($fileType)) {
|
||||
return $fileType;
|
||||
}
|
||||
|
||||
if (substr($fileType, 0, 1) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $fileType;
|
||||
}
|
||||
}
|
|
@ -24,4 +24,9 @@ class ClientCapabilities
|
|||
* @var bool|null
|
||||
*/
|
||||
public $xcacheProvider;
|
||||
|
||||
/**
|
||||
* @var WorkspaceClientCapabilities
|
||||
*/
|
||||
public $workspace;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class ConfigurationItem
|
||||
{
|
||||
/**
|
||||
* The scope to get the configuration section for.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $scopeUri;
|
||||
|
||||
/**
|
||||
* The configuration section asked for.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $section;
|
||||
|
||||
public function __construct(string $section = null, string $scopeUri = null)
|
||||
{
|
||||
$this->section = $section;
|
||||
$this->scopeUri = $scopeUri;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class WorkspaceClientCapabilities
|
||||
{
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
public $configuration;
|
||||
}
|
|
@ -3,11 +3,12 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Server;
|
||||
|
||||
use LanguageServer\{LanguageClient, PhpDocumentLoader};
|
||||
use LanguageServer\{Indexer, LanguageClient, Options, PhpDocumentLoader};
|
||||
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
||||
use LanguageServer\Protocol\{
|
||||
FileChangeType,
|
||||
FileEvent,
|
||||
MessageType,
|
||||
SymbolInformation,
|
||||
SymbolDescriptor,
|
||||
ReferenceInformation,
|
||||
|
@ -45,6 +46,11 @@ class Workspace
|
|||
*/
|
||||
private $sourceIndex;
|
||||
|
||||
/**
|
||||
* @var Indexer
|
||||
*/
|
||||
private $indexer;
|
||||
|
||||
/**
|
||||
* @var \stdClass
|
||||
*/
|
||||
|
@ -60,11 +66,20 @@ class Workspace
|
|||
* @param ProjectIndex $projectIndex Index that is used to wait for full index completeness
|
||||
* @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request
|
||||
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
|
||||
* @param Indexer $indexer
|
||||
* @param \stdClass $composerLock The parsed composer.lock of the project, if any
|
||||
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
|
||||
*/
|
||||
public function __construct(LanguageClient $client, ProjectIndex $projectIndex, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null)
|
||||
{
|
||||
public function __construct(
|
||||
LanguageClient $client,
|
||||
ProjectIndex $projectIndex,
|
||||
DependenciesIndex $dependenciesIndex,
|
||||
Index $sourceIndex,
|
||||
Indexer $indexer,
|
||||
\stdClass $composerLock = null,
|
||||
PhpDocumentLoader $documentLoader,
|
||||
\stdClass $composerJson = null
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->sourceIndex = $sourceIndex;
|
||||
$this->projectIndex = $projectIndex;
|
||||
|
@ -72,10 +87,12 @@ class Workspace
|
|||
$this->composerLock = $composerLock;
|
||||
$this->documentLoader = $documentLoader;
|
||||
$this->composerJson = $composerJson;
|
||||
$this->indexer = $indexer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The workspace symbol request is sent from the client to the server to list project-wide symbols matching the query string.
|
||||
* The workspace symbol request is sent from the client to the server to list
|
||||
* project-wide symbols matching the query string.
|
||||
*
|
||||
* @param string $query
|
||||
* @return Promise <SymbolInformation[]>
|
||||
|
@ -98,7 +115,8 @@ class Workspace
|
|||
}
|
||||
|
||||
/**
|
||||
* The watched files notification is sent from the client to the server when the client detects changes to files watched by the language client.
|
||||
* The watched files notification is sent from the client to the server when
|
||||
* the client detects changes to files watched by the language client.
|
||||
*
|
||||
* @param FileEvent[] $changes
|
||||
* @return void
|
||||
|
@ -113,7 +131,8 @@ class Workspace
|
|||
}
|
||||
|
||||
/**
|
||||
* The workspace references request is sent from the client to the server to locate project-wide references to a symbol given its description / metadata.
|
||||
* The workspace references request is sent from the client to the server to
|
||||
* locate project-wide references to a symbol given its description / metadata.
|
||||
*
|
||||
* @param SymbolDescriptor $query Partial metadata about the symbol that is being searched for.
|
||||
* @param string[] $files An optional list of files to restrict the search to.
|
||||
|
@ -174,4 +193,66 @@ class Workspace
|
|||
}
|
||||
return $dependencyReferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* A notification sent from the client to the server to signal the change of configuration settings.
|
||||
*
|
||||
* @param mixed $settings Settings as JSON object structure with php as primary key
|
||||
* @return Promise
|
||||
*/
|
||||
public function didChangeConfiguration($settings): Promise
|
||||
{
|
||||
return coroutine(function () use ($settings) {
|
||||
if (!property_exists($settings, 'php') || $settings->php === new \stdClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$mapper = new \JsonMapper();
|
||||
$options = $mapper->map($settings->php, new Options);
|
||||
|
||||
// handle options for indexer
|
||||
$currentIndexerOptions = $this->indexer->getOptions();
|
||||
$this->indexer->setOptions($options);
|
||||
|
||||
if ($this->hasIndexerOptionsChanged($currentIndexerOptions, $options)) {
|
||||
if ($this->indexer->isIndexing()) {
|
||||
yield $this->indexer->cancel();
|
||||
}
|
||||
|
||||
$this->projectIndex->wipe();
|
||||
yield $this->indexer->index();
|
||||
}
|
||||
} catch (\JsonMapper_Exception $exception) {
|
||||
$this->client->window->showMessage(
|
||||
MessageType::ERROR,
|
||||
'Settings could not be applied. For more information see logs.'
|
||||
);
|
||||
$this->client->window->logMessage(MessageType::ERROR, $exception->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare current options with new
|
||||
*
|
||||
* When the new options differ from the current, then we need start
|
||||
* to reindex the project folder.
|
||||
*
|
||||
* @param Options $current
|
||||
* @param Options $new
|
||||
* @return bool
|
||||
*/
|
||||
private function hasIndexerOptionsChanged(Options $current, Options $new): bool
|
||||
{
|
||||
$properties = ['fileTypes'];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if ($current->{$property} !== $new->{$property}) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace LanguageServer\Tests;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\LanguageServer;
|
||||
use LanguageServer\Options;
|
||||
use LanguageServer\Protocol\{
|
||||
Message,
|
||||
ClientCapabilities,
|
||||
|
@ -55,19 +56,25 @@ class LanguageServerTest extends TestCase
|
|||
$promise = new Promise;
|
||||
$input = new MockProtocolStream;
|
||||
$output = new MockProtocolStream;
|
||||
$output->on('message', function (Message $msg) use ($promise) {
|
||||
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||
$output->on('message', function (Message $msg) use ($promise, $input) {
|
||||
if ($msg->body->method === 'workspace/configuration') {
|
||||
$result = new \stdClass();
|
||||
$result->fileTypes = ['.php'];
|
||||
|
||||
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
|
||||
} elseif ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||
if ($msg->body->params->type === MessageType::ERROR) {
|
||||
$promise->reject(new Exception($msg->body->params->message));
|
||||
} else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
|
||||
$promise->fulfill();
|
||||
$promise->fulfill(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
$server = new LanguageServer($input, $output);
|
||||
$capabilities = new ClientCapabilities;
|
||||
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid());
|
||||
$promise->wait();
|
||||
$server->initialized();
|
||||
$this->assertTrue($promise->wait());
|
||||
}
|
||||
|
||||
public function testIndexingWithFilesAndContentRequests()
|
||||
|
@ -80,7 +87,12 @@ class LanguageServerTest extends TestCase
|
|||
$output = new MockProtocolStream;
|
||||
$run = 1;
|
||||
$output->on('message', function (Message $msg) use ($promise, $input, $rootPath, &$filesCalled, &$contentCalled, &$run) {
|
||||
if ($msg->body->method === 'textDocument/xcontent') {
|
||||
if ($msg->body->method === 'workspace/configuration') {
|
||||
$result = new \stdClass();
|
||||
$result->fileTypes = ['.php'];
|
||||
|
||||
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
|
||||
} elseif ($msg->body->method === 'textDocument/xcontent') {
|
||||
// Document content requested
|
||||
$contentCalled = true;
|
||||
$textDocumentItem = new TextDocumentItem;
|
||||
|
@ -114,9 +126,37 @@ class LanguageServerTest extends TestCase
|
|||
$capabilities = new ClientCapabilities;
|
||||
$capabilities->xfilesProvider = true;
|
||||
$capabilities->xcontentProvider = true;
|
||||
$server->initialize($capabilities, $rootPath, getmypid());
|
||||
$server->initialize($capabilities, $rootPath, getmypid())->wait();
|
||||
$server->initialized();
|
||||
$promise->wait();
|
||||
$this->assertTrue($filesCalled);
|
||||
$this->assertTrue($contentCalled);
|
||||
}
|
||||
|
||||
public function testIndexingMultipleFileTypes()
|
||||
{
|
||||
$promise = new Promise;
|
||||
$input = new MockProtocolStream;
|
||||
$output = new MockProtocolStream;
|
||||
|
||||
$output->on('message', function (Message $msg) use ($promise, $input) {
|
||||
if ($msg->body->method === 'workspace/configuration') {
|
||||
$result = new \stdClass();
|
||||
$result->fileTypes = ['.php', '.inc'];
|
||||
|
||||
$input->write(new Message(new AdvancedJsonRpc\SuccessResponse($msg->body->id, [$result])));
|
||||
} elseif ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
|
||||
if ($msg->body->params->type === MessageType::ERROR) {
|
||||
$promise->reject(new Exception($msg->body->params->message));
|
||||
} elseif (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
|
||||
$promise->fulfill(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
$server = new LanguageServer($input, $output);
|
||||
$capabilities = new ClientCapabilities;
|
||||
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid());
|
||||
$server->initialized();
|
||||
$this->assertTrue($promise->wait());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Options;
|
||||
|
||||
class OptionsTest extends TestCase
|
||||
{
|
||||
public function testFileTypesOption()
|
||||
{
|
||||
$expected = [
|
||||
'.php',
|
||||
'.valid'
|
||||
];
|
||||
|
||||
$options = new Options;
|
||||
$options->setFileTypes([
|
||||
'.php',
|
||||
false,
|
||||
12345,
|
||||
'.valid'
|
||||
]);
|
||||
|
||||
$this->assertSame($expected, $options->fileTypes);
|
||||
}
|
||||
}
|
|
@ -5,9 +5,7 @@ namespace LanguageServer\Tests\Server;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{
|
||||
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
use LanguageServer\{Options, Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
|
||||
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Protocol\{Position, Location, Range};
|
||||
|
@ -46,6 +44,7 @@ abstract class ServerTestCase extends TestCase
|
|||
|
||||
public function setUp()
|
||||
{
|
||||
$options = new Options();
|
||||
$sourceIndex = new Index;
|
||||
$dependenciesIndex = new DependenciesIndex;
|
||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||
|
@ -55,7 +54,7 @@ abstract class ServerTestCase extends TestCase
|
|||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
|
||||
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
|
||||
$this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader);
|
||||
$this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $this->documentLoader);
|
||||
|
||||
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
|
||||
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
/**
|
||||
* Copyright (c) 2018 Jürgen Steitz
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LanguageServer\Tests\Server\Workspace;
|
||||
|
||||
use Exception;
|
||||
use LanguageServer\Cache\FileSystemCache;
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use LanguageServer\FilesFinder\FileSystemFilesFinder;
|
||||
use LanguageServer\Index\DependenciesIndex;
|
||||
use LanguageServer\Index\Index;
|
||||
use LanguageServer\Index\ProjectIndex;
|
||||
use LanguageServer\Indexer;
|
||||
use LanguageServer\LanguageClient;
|
||||
use LanguageServer\Options;
|
||||
use LanguageServer\PhpDocumentLoader;
|
||||
use LanguageServer\Protocol\Message;
|
||||
use LanguageServer\Protocol\MessageType;
|
||||
use LanguageServer\Server;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\Tests\Server\ServerTestCase;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
class DidChangeConfigurationTest extends ServerTestCase
|
||||
{
|
||||
public function testFailsWithInvalidOptionsTypeOrFormat()
|
||||
{
|
||||
$promise = new Promise;
|
||||
$sourceIndex = new Index;
|
||||
$dependenciesIndex = new DependenciesIndex;
|
||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||
$projectIndex->setComplete();
|
||||
|
||||
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
|
||||
$filesFinder = new FileSystemFilesFinder;
|
||||
$cache = new FileSystemCache;
|
||||
$initialOptions = new Options;
|
||||
|
||||
$input = new MockProtocolStream;
|
||||
$output = new MockProtocolStream;
|
||||
|
||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||
$client = new LanguageClient($input, $output);
|
||||
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
|
||||
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
|
||||
$indexer = new Indexer(
|
||||
$filesFinder,
|
||||
$rootPath,
|
||||
$client,
|
||||
$cache,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$documentLoader,
|
||||
$initialOptions
|
||||
);
|
||||
$workspace = new Server\Workspace(
|
||||
$client,
|
||||
$projectIndex,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$initialOptions,
|
||||
null,
|
||||
$documentLoader
|
||||
);
|
||||
|
||||
$output->on('message', function (Message $msg) use ($promise) {
|
||||
if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) {
|
||||
$hasMessage = strpos(
|
||||
$msg->body->params->message,
|
||||
'Settings could not be applied. For more information see logs.'
|
||||
) !== false;
|
||||
|
||||
if ($msg->body->params->type === MessageType::ERROR && $hasMessage) {
|
||||
$promise->fulfill(true);
|
||||
}
|
||||
|
||||
if ($msg->body->params->type !== MessageType::ERROR) {
|
||||
$promise->reject(new Exception($msg->body->params->message));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$settings = new \stdClass();
|
||||
$settings->php = new \stdClass();
|
||||
$settings->php->fileTypes = 'not an array';
|
||||
|
||||
$workspace->didChangeConfiguration($settings);
|
||||
$this->assertTrue($promise->wait());
|
||||
}
|
||||
|
||||
public function testNoChangedOptions()
|
||||
{
|
||||
$promise = new Promise;
|
||||
$sourceIndex = new Index;
|
||||
$dependenciesIndex = new DependenciesIndex;
|
||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||
$projectIndex->setComplete();
|
||||
|
||||
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
|
||||
$filesFinder = new FileSystemFilesFinder;
|
||||
$cache = new FileSystemCache;
|
||||
$initialOptions = new Options;
|
||||
|
||||
$input = new MockProtocolStream;
|
||||
$output = new MockProtocolStream;
|
||||
|
||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||
$client = new LanguageClient($input, $output);
|
||||
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
|
||||
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
|
||||
$indexer = new Indexer(
|
||||
$filesFinder,
|
||||
$rootPath,
|
||||
$client,
|
||||
$cache,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$documentLoader,
|
||||
$initialOptions
|
||||
);
|
||||
$workspace = new Server\Workspace(
|
||||
$client,
|
||||
$projectIndex,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$initialOptions,
|
||||
null,
|
||||
$documentLoader
|
||||
);
|
||||
|
||||
$output->on('message', function (Message $msg) use ($promise) {
|
||||
$promise->reject(new Exception($msg->body->message));
|
||||
});
|
||||
|
||||
$settings = new \stdClass();
|
||||
$settings->php = new \stdClass();
|
||||
$settings->php->fileTypes = ['.php'];
|
||||
|
||||
$this->expectException(\LogicException::class);
|
||||
$workspace->didChangeConfiguration($settings);
|
||||
$promise->wait();
|
||||
}
|
||||
|
||||
public function testDetectsChangedOptions()
|
||||
{
|
||||
$promise = new Promise;
|
||||
$sourceIndex = new Index;
|
||||
$dependenciesIndex = new DependenciesIndex;
|
||||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||
$projectIndex->setComplete();
|
||||
|
||||
$rootPath = realpath(__DIR__ . '/../../../fixtures/');
|
||||
$filesFinder = new FileSystemFilesFinder;
|
||||
$cache = new FileSystemCache;
|
||||
$initialOptions = new Options;
|
||||
|
||||
$input = new MockProtocolStream;
|
||||
$output = new MockProtocolStream;
|
||||
|
||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||
$client = new LanguageClient($input, $output);
|
||||
$documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
|
||||
$textDocument = new Server\TextDocument($documentLoader, $definitionResolver, $client, $projectIndex);
|
||||
$indexer = new Indexer(
|
||||
$filesFinder,
|
||||
$rootPath,
|
||||
$client,
|
||||
$cache,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$documentLoader,
|
||||
$initialOptions
|
||||
);
|
||||
$workspace = new Server\Workspace(
|
||||
$client,
|
||||
$projectIndex,
|
||||
$dependenciesIndex,
|
||||
$sourceIndex,
|
||||
$initialOptions,
|
||||
null,
|
||||
$documentLoader
|
||||
);
|
||||
|
||||
$output->on('message', function (Message $msg) use ($promise) {
|
||||
if ($msg->body->method === 'window/showMessage' && $promise->state === Promise::PENDING) {
|
||||
$hasMessage = strpos(
|
||||
$msg->body->params->message,
|
||||
'You must restart your editor for the changes to take effect.'
|
||||
) !== false;
|
||||
|
||||
if ($msg->body->params->type === MessageType::INFO && $hasMessage) {
|
||||
$promise->fulfill(true);
|
||||
}
|
||||
|
||||
if ($msg->body->params->type === MessageType::ERROR) {
|
||||
$promise->reject(new Exception($msg->body->params->message));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$settings = new \stdClass();
|
||||
$settings->php = new \stdClass();
|
||||
$settings->php->fileTypes = ['.php', '.php5']; // default is only .php
|
||||
|
||||
$workspace->didChangeConfiguration($settings);
|
||||
$this->assertTrue($promise->wait());
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Tests\Server\Workspace;
|
||||
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\{DefinitionResolver, LanguageClient, PhpDocumentLoader, Server};
|
||||
use LanguageServer\{DefinitionResolver, LanguageClient, Options, PhpDocumentLoader, Server};
|
||||
use LanguageServer\Index\{DependenciesIndex, Index, ProjectIndex};
|
||||
use LanguageServer\Protocol\{FileChangeType, FileEvent, Message};
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
|
@ -16,11 +16,12 @@ class DidChangeWatchedFilesTest extends ServerTestCase
|
|||
{
|
||||
public function testDeletingFileClearsAllDiagnostics()
|
||||
{
|
||||
$options = new Options();
|
||||
$client = new LanguageClient(new MockProtocolStream(), $writer = new MockProtocolStream());
|
||||
$projectIndex = new ProjectIndex($sourceIndex = new Index(), $dependenciesIndex = new DependenciesIndex());
|
||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||
$loader = new PhpDocumentLoader(new FileSystemContentRetriever(), $projectIndex, $definitionResolver);
|
||||
$workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $loader, null);
|
||||
$workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, $options, null, $loader, null);
|
||||
|
||||
$fileEvent = new FileEvent('my uri', FileChangeType::DELETED);
|
||||
|
||||
|
|
Loading…
Reference in New Issue