Skip to content
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

Allow configurable file extension for indexing #308

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cdb5b56
Add support to index multiple file extensions
Feb 18, 2017
5f096c4
Add test for indexing multiple file types
Feb 18, 2017
7dc4477
Fix wrong phpDoc type
Feb 18, 2017
f7175bc
Filter invalid file types and use default list as fallback
Feb 18, 2017
9433694
Let JsonMapper intialize the options
Feb 18, 2017
39cfbda
Add test for fileTypes option
Feb 18, 2017
3c33e7f
Initialize options with default values when not provided by client
Feb 18, 2017
d2e5048
Update testIndexingMultipleFileTypes
Feb 18, 2017
b9d0d1b
Add missing namespace in OptionsTest
Feb 18, 2017
9067b44
Fix wrong classname for options test
Feb 18, 2017
1e319c7
Wipe index when on configuration change
Feb 24, 2017
58c82e6
Add list of valid indexer options
Mar 2, 2017
44a942e
Implement didChangeConfiguration event
Mar 2, 2017
940eb97
Pass options and indexer to workspace
Mar 2, 2017
5b1b6bf
Add tests
Mar 2, 2017
707c97f
Merge branch 'master' of github.com:felixfbecker/php-language-server …
Mar 2, 2017
1e73d08
Improve gettting changed options
Mar 4, 2017
1f90b4e
Update options one by one to update all instance
Mar 4, 2017
ca225ff
Remove emitting wipe events
Mar 4, 2017
c4568bf
Accept different types/formats from clients
Mar 4, 2017
5308e7a
Add new tests and update old ones
Mar 4, 2017
a06057b
Fix phpcs warnings/errors
Mar 4, 2017
23a40f0
Let didChangeConfiguration decide what options are interesting for th…
Mar 4, 2017
f4f1067
Change didChangeConfiguration doc to protocol wording
Mar 4, 2017
9cc2736
Merge remote-tracking branch 'upstream/master' into feature/allow-con…
JSteitz Aug 29, 2018
09fbec2
Refactor pull request
JSteitz Aug 29, 2018
a5417cd
Fix risky test warning
JSteitz Aug 29, 2018
e317e8c
Start indexing after initialization
JSteitz Aug 31, 2018
a1c3845
WIP: Implement didChangeConfiguration with reindexing
JSteitz Aug 31, 2018
a1e5654
WIP: Implement didChangeConfiguration with reindexing
JSteitz Aug 31, 2018
a81bed9
Partial work on feedback
JSteitz Aug 31, 2018
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
1 change: 1 addition & 0 deletions fixtures/different_extension.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php
10 changes: 10 additions & 0 deletions src/Index/AbstractAggregateIndex.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,14 @@ public function getReferenceUris(string $fqn): array
}
return $refs;
}

/**
* Wipe all indexes for a reindex
*/
public function wipe()
{
foreach ($this->getIndexes() as $index) {
$index->wipe();
}
}
}
8 changes: 8 additions & 0 deletions src/Index/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,12 @@ public function serialize()
'staticComplete' => $this->staticComplete
]);
}

public function wipe()
{
$this->definitions = [];
$this->references = [];
$this->complete = false;
$this->staticComplete = false;
}
}
30 changes: 19 additions & 11 deletions src/Indexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,20 @@ class Indexer
private $composerJson;

/**
* @param FilesFinder $filesFinder
* @param string $rootPath
* @param LanguageClient $client
* @param Cache $cache
* @param DependenciesIndex $dependenciesIndex
* @param Index $sourceIndex
* @param PhpDocumentLoader $documentLoader
* @param \stdClass|null $composerLock
* @var Options
*/
private $options;

/**
* @param FilesFinder $filesFinder
* @param string $rootPath
* @param LanguageClient $client
* @param Cache $cache
* @param DependenciesIndex $dependenciesIndex
* @param Index $sourceIndex
* @param PhpDocumentLoader $documentLoader
* @param \stdClass|null $composerLock
* @param Options|null $options
*/
public function __construct(
FilesFinder $filesFinder,
Expand All @@ -83,7 +89,8 @@ public function __construct(
Index $sourceIndex,
PhpDocumentLoader $documentLoader,
\stdClass $composerLock = null,
\stdClass $composerJson = null
\stdClass $composerJson = null,
Options $options = null
) {
$this->filesFinder = $filesFinder;
$this->rootPath = $rootPath;
Expand All @@ -94,6 +101,7 @@ public function __construct(
$this->documentLoader = $documentLoader;
$this->composerLock = $composerLock;
$this->composerJson = $composerJson;
$this->options = $options;
}

/**
Expand All @@ -104,8 +112,8 @@ public function __construct(
public function index(): Promise
{
return coroutine(function () {

$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
$fileTypes = implode(',', $this->options->fileTypes);
$pattern = Path::makeAbsolute('**/*{' . $fileTypes . '}', $this->rootPath);
felixfbecker marked this conversation as resolved.
Show resolved Hide resolved
$uris = yield $this->filesFinder->find($pattern);

$count = count($uris);
Expand Down
13 changes: 9 additions & 4 deletions src/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
* @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 Options $initializationOptions The options send from client to initialize the server
* @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, Options $initializationOptions = null): Promise
{
return coroutine(function () use ($capabilities, $rootPath, $processId) {
return coroutine(function () use ($capabilities, $rootPath, $processId, $initializationOptions) {

if ($capabilities->xfilesProvider) {
$this->filesFinder = new ClientFilesFinder($this->client);
Expand All @@ -190,6 +191,7 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath =
$this->projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex, $this->composerJson);
$stubsIndex = StubsIndex::read();
$this->globalIndex = new GlobalIndex($stubsIndex, $this->projectIndex);
$initializationOptions = $initializationOptions ?? new Options;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way $initializationOptions is not an instance of Options if provided

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't get you here, is this comment a part from another comment?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $initializedOptions is not null, it will be stdClass because it's not typed. I also think you don't account for the nesting under ->php here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For $initializationOptions you have more freedom how and which options will be send.

The $initializationOptions parameter ist typed in initialize() and is only passed as is to coroutine(), so it should be enough only to check for null.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You pass this variable to the Indexer further down, which accepts Options, not stdClass

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really sorry but I don't see a problem here.
$initializationOptions is always of type Options.

At which point will it be a stdClass?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I think I missed the type annotation because I looked at the closure use. But shouldn't this be the same object as passed to didChangeConfiguration? And will this break passing custom properties from the client in initializationOptions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializationOptions can be completely different to didChangeConfiguration.
And actually they are not the same in this PR. While initializationOptions returns only defined options in the extension, didChangeConfiguration will return the whole section with the section name as main key.

initializationOptions

{
  "fileTypes": [
    ".php"
  ]
}

didChangeConfiguration

{
  "php": {
    "executablePath": "\/usr\/local\/opt\/php71\/bin\/php",
    "fileTypes": [
      ".php"
    ],
    "suggest": {
      "basic": true
    },
    "validate": {
      "enable": true,
      "executablePath": "\/usr\/local\/opt\/php71\/bin\/php",
      "run": "onSave"
    }
  }
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's up to us to defined - I am just saying it's a bit confusing that they are different. It would be much easier for a client if we just document please pass the didChangeConfiguration settings as initializationOptions


// The DefinitionResolver should look in stubs, the project source and dependencies
$this->definitionResolver = new DefinitionResolver($this->globalIndex);
Expand Down Expand Up @@ -235,7 +237,8 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath =
$sourceIndex,
$this->documentLoader,
$this->composerLock,
$this->composerJson
$this->composerJson,
$initializationOptions
);
$indexer->index()->otherwise('\\LanguageServer\\crash');
}
Expand All @@ -259,7 +262,9 @@ public function initialize(ClientCapabilities $capabilities, string $rootPath =
$sourceIndex,
$this->composerLock,
$this->documentLoader,
$this->composerJson
$this->composerJson,
$indexer,
$initializationOptions
);
}

Expand Down
64 changes: 64 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace LanguageServer;

class Options
{
/**
* Filetypes the indexer should process
*
* @var string[]
*/
public $fileTypes = ['.php'];

/**
* List of options that affect the indexer
*/
private $indexerOptions = ['fileTypes'];

/**
* Validate/Filter input and set options for file types
*
* @param array $fileTypes List of file types
*/
public function setFileTypes(array $fileTypes)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are not needed if you let JsonMapper take care of validation

{
$fileTypes = filter_var_array($fileTypes, FILTER_SANITIZE_STRING);
$fileTypes = filter_var($fileTypes, FILTER_CALLBACK, ['options' => [$this, 'filterFileTypes']]); // validate file type format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one space to much here.

$fileTypes = array_filter($fileTypes, 'strlen'); // filter empty items
$fileTypes = array_values($fileTypes); //rebase indexes

$this->fileTypes = !empty($fileTypes) ? $fileTypes : $this->fileTypes;
}

/**
* Get list with options that affect the indexer
*
* @return array
*/
public function getIndexerOptions(): array
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to keep Options a dumb data object (like other protocol structures).
Checking if reindexing is needed should be done solely in didChangeConfiguration.

{
return $this->indexerOptions;
}

/**
* 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;
}
}
83 changes: 81 additions & 2 deletions src/Server/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace LanguageServer\Server;

use LanguageServer\{LanguageClient, Project, PhpDocumentLoader};
use LanguageServer\{LanguageClient, Project, PhpDocumentLoader, Options, Indexer};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{
FileChangeType,
Expand Down Expand Up @@ -45,6 +45,16 @@ class Workspace
*/
private $sourceIndex;

/**
* @var Options
*/
private $options;

/**
* @var Indexer
*/
private $indexer;

/**
* @var \stdClass
*/
Expand All @@ -62,8 +72,10 @@ class Workspace
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
* @param \stdClass $composerLock The parsed composer.lock of the project, if any
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
* @param Indexer $indexer
* @param Options $options
*/
public function __construct(LanguageClient $client, ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null)
public function __construct(LanguageClient $client, ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null, Indexer $indexer = null, Options $options = null)
{
$this->client = $client;
$this->sourceIndex = $sourceIndex;
Expand All @@ -72,6 +84,8 @@ public function __construct(LanguageClient $client, ProjectIndex $index, Depende
$this->composerLock = $composerLock;
$this->documentLoader = $documentLoader;
$this->composerJson = $composerJson;
$this->indexer = $indexer;
$this->options = $options;
}

/**
Expand Down Expand Up @@ -200,4 +214,69 @@ public function xdependencies(): array
}
return $dependencyReferences;
}

/**
* Fires when client changes settings in the client
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to the protocol wording:

A notification sent from the client to the server to signal the change of configuration settings.

https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#didchangeconfiguration-notification

*
* The default paramter type is Options but it also accepts different types
* which will be transformed on demand.
*
* Currently only the vscode format is supported
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? Where is the problem in saying the PHP LS reads settings under the php key?

*
* @param mixed|null $settings
* @return void
*/
public function didChangeConfiguration($settings = null)
{
if ($settings === null) {
return;
}

// VSC sends the settings with the config section as main key
if ($settings instanceof \stdClass && $settings->phpIntelliSense) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not happy with this part.
The documentation is lacking in which format the settings are passed to the server and why they do it that way.
With all the different tests I did, they always send it like that:

{
    "ExtensionConfigSectionName": { // defined in the package.json in the vscode extensions
        ... the actual settings ...
    }
}

$mapper = new \JsonMapper();
$settings = $mapper->map($settings->phpIntelliSense, new Options);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use php as the config section? "IntelliSense" is a term only associated with VS and VS Code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure we can.

We only have to lookout for possible conflicts in the future when we use php as config section.

Possible config naming:

  • php.indexer.*
  • php.completion.* (possible name for a feature request, but can be ignored now)
  • php.intellisense.* (previous name)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

php.fileExtensions should be fine. I don't think we should concern user with the concept of an "indexer"

}

if (!($settings instanceof Options)) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik JsonMapper will throw when the schema doesn't match

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have to tell him to that?
Because for me JsonMapper never threw an exception when the schema didn't match.
It silently ignored invalid properties and used the defaults in the Options.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What case exactly did not throw an exception? It could be because you typed setFileTypes() as taking array, not string[]

return;
}

$changedOptions = $this->getChangedOptions($settings);

if (empty($changedOptions)) {
return;
}

foreach (get_object_vars($settings) as $prop => $val) {
$this->options->$prop = $val;
}

if ($this->indexer && !empty(array_intersect($changedOptions, $this->options->getIndexerOptions()))) {
// check list of options that changed since last time against the list of valid indexer options

// wipe main index and start reindexing
$this->index->wipe();
$this->indexer->index()->otherwise('\\LanguageServer\\crash');
}
}

/**
* Get a list with all options that changed since last time
*
* @param Options $settings
* @return array List with changed options
*/
private function getChangedOptions(Options $settings): array
{
$old = get_object_vars($this->options);
$new = get_object_vars($settings);
$changed = array_udiff($old, $new, function($a, $b) {
// custom callback since array_diff uses strings for comparison

return $a <=> $b;
});

return array_keys($changed);
}
}
28 changes: 28 additions & 0 deletions tests/LanguageServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use PHPUnit\Framework\TestCase;
use LanguageServer\LanguageServer;
use LanguageServer\Options;
use LanguageServer\Protocol\{
Message,
ClientCapabilities,
Expand Down Expand Up @@ -117,4 +118,31 @@ public function testIndexingWithFilesAndContentRequests()
$this->assertTrue($filesCalled);
$this->assertTrue($contentCalled);
}

public function testIndexingMultipleFileTypes()
{
$promise = new Promise;
$input = new MockProtocolStream;
$output = new MockProtocolStream;
$options = new Options;

$options->setFileTypes([
'.php',
'.inc'
]);

$output->on('message', function (Message $msg) use ($promise, &$foundFiles) {
if ($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 (strpos($msg->body->params->message, 'All 27 PHP files parsed') !== false) {
$promise->fulfill();
}
}
});
$server = new LanguageServer($input, $output);
$capabilities = new ClientCapabilities;
$server->initialize($capabilities, realpath(__DIR__ . '/../fixtures'), getmypid(), $options);
$promise->wait();
}
}
28 changes: 28 additions & 0 deletions tests/OptionsTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading