diff --git a/.gitignore b/.gitignore index c018fa6..4791ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ .phpls/ composer.lock +stubs diff --git a/.travis.yml b/.travis.yml index 0199d52..7fb9c9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,11 @@ services: cache: directories: - - vendor + - $HOME/.composer/cache install: - composer install + - composer run-script parse-stubs script: - vendor/bin/phpcs -n diff --git a/README.md b/README.md index de9167b..9f50476 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Non-Standard: An empty query will return _all_ symbols found in the workspace. PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings. Errors/Warnings from the `vendor` directory are ignored. +### Stubs for PHP built-ins + +Completion, type resolval etc. will use the standard PHP library and common extensions. + ### What is considered a definition? Globally searchable definitions are: @@ -131,6 +135,11 @@ Simply run and you will get the latest stable release and all dependencies. Running `composer update` will update the server to the latest non-breaking version. +After installing the language server and its dependencies, +you must parse the stubs for standard PHP symbols and save the index for fast initialization. + + composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs + ## Running Start the language server with @@ -178,6 +187,9 @@ Clone the repository and run composer install to install dependencies. +Then parse the stubs with + + composer run-script parse-stubs Run the tests with diff --git a/composer.json b/composer.json index 224d3d0..898da3b 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,9 @@ "refactor" ], "bin": ["bin/php-language-server.php"], + "scripts": { + "parse-stubs": "LanguageServer\\ComposerScripts::parseStubs" + }, "require": { "php": ">=7.0", "nikic/php-parser": "dev-master#e52ffc4447e034514339a03b450aab9cd625e37c", @@ -32,8 +35,27 @@ "netresearch/jsonmapper": "^1.0", "webmozart/path-util": "^2.3", "webmozart/glob": "^4.1", - "sabre/uri": "^2.0" + "sabre/uri": "^2.0", + "JetBrains/phpstorm-stubs": "dev-master" }, + "repositories": [ + { + "type": "package", + "package": { + "name": "JetBrains/phpstorm-stubs", + "version": "dev-master", + "dist": { + "url": "https://github.com/JetBrains/phpstorm-stubs/archive/master.zip", + "type": "zip" + }, + "source": { + "url": "https://github.com/JetBrains/phpstorm-stubs", + "type": "git", + "reference": "master" + } + } + } + ], "minimum-stability": "dev", "prefer-stable": true, "autoload": { diff --git a/src/CompletionProvider.php b/src/CompletionProvider.php index 5d65ea3..fb9ebb8 100644 --- a/src/CompletionProvider.php +++ b/src/CompletionProvider.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer; use PhpParser\Node; +use LanguageServer\Index\ReadableIndex; use LanguageServer\Protocol\{ TextEdit, Range, @@ -97,13 +98,18 @@ class CompletionProvider private $project; /** - * @param DefinitionResolver $definitionResolver - * @param Project $project + * @var ReadableIndex */ - public function __construct(DefinitionResolver $definitionResolver, Project $project) + private $index; + + /** + * @param DefinitionResolver $definitionResolver + * @param ReadableIndex $index + */ + public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index) { $this->definitionResolver = $definitionResolver; - $this->project = $project; + $this->index = $index; } /** @@ -153,7 +159,7 @@ class CompletionProvider } } - foreach ($this->project->getDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions() as $fqn => $def) { foreach ($prefixes as $prefix) { if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { $list->items[] = CompletionItem::fromDefinition($def); @@ -185,7 +191,9 @@ class CompletionProvider // Get the definition for the used namespace, class-like, function or constant // And save it under the alias $fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name); - $aliasedDefs[$use->alias] = $this->project->getDefinition($fqn); + if ($def = $this->index->getDefinition($fqn)) { + $aliasedDefs[$use->alias] = $def; + } } } else { // Use statements are always the first statements in a namespace @@ -206,7 +214,7 @@ class CompletionProvider // Additionally, suggest global symbols that either // - start with the current namespace + prefix, if the Name node is not fully qualified // - start with just the prefix, if the Name node is fully qualified - foreach ($this->project->getDefinitions() as $fqn => $def) { + foreach ($this->index->getDefinitions() as $fqn => $def) { if ( $def->isGlobal // exclude methods, properties etc. && ( @@ -326,7 +334,7 @@ class CompletionProvider } if ($level instanceof Node\Expr\Closure) { foreach ($level->uses as $use) { - if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { + if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) { $vars[$use->var] = $use; } } diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php new file mode 100644 index 0000000..67060bc --- /dev/null +++ b/src/ComposerScripts.php @@ -0,0 +1,57 @@ +find("$stubsLocation/**/*.php"); + + foreach ($uris as $uri) { + echo "Parsing $uri\n"; + $content = yield $contentRetriever->retrieve($uri); + + // Change URI to phpstubs:// + $parts = Uri\parse($uri); + $parts['path'] = Path::makeRelative($parts['path'], $stubsLocation); + $parts['scheme'] = 'phpstubs'; + $uri = Uri\build($parts); + + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + } + + echo "Saving Index\n"; + + $index->save(); + + echo "Finished\n"; + })->wait(); + } +} diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 87c5aa0..a2f30e4 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -7,15 +7,14 @@ use PhpParser\Node; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use LanguageServer\Protocol\SymbolInformation; -use Sabre\Event\Promise; -use function Sabre\Event\coroutine; +use LanguageServer\Index\ReadableIndex; class DefinitionResolver { /** - * @var \LanguageServer\Project + * @var \LanguageServer\Index */ - private $project; + private $index; /** * @var \phpDocumentor\Reflection\TypeResolver @@ -27,9 +26,12 @@ class DefinitionResolver */ private $prettyPrinter; - public function __construct(Project $project) + /** + * @param ReadableIndex $index + */ + public function __construct(ReadableIndex $index) { - $this->project = $project; + $this->index = $index; $this->typeResolver = new TypeResolver; $this->prettyPrinter = new PrettyPrinter; } @@ -147,8 +149,8 @@ class DefinitionResolver // http://php.net/manual/en/language.namespaces.fallback.php $parent = $node->getAttribute('parentNode'); $globalFallback = $parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall; - // Return the Definition object from the project index - return $this->project->getDefinition($fqn, $globalFallback); + // Return the Definition object from the index index + return $this->index->getDefinition($fqn, $globalFallback); } /** @@ -403,7 +405,7 @@ class DefinitionResolver return new Types\Mixed; } $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -414,7 +416,7 @@ class DefinitionResolver } // Resolve constant $fqn = (string)($expr->getAttribute('namespacedName') ?? $expr->name); - $def = $this->project->getDefinition($fqn, true); + $def = $this->index->getDefinition($fqn, true); if ($def !== null) { return $def->type; } @@ -443,7 +445,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\MethodCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->index->getDefinition($fqn); if ($def !== null) { return $def->type; } @@ -466,7 +468,7 @@ class DefinitionResolver if ($expr instanceof Node\Expr\StaticCall) { $fqn .= '()'; } - $def = $this->project->getDefinition($fqn); + $def = $this->index->getDefinition($fqn); if ($def === null) { return new Types\Mixed; } diff --git a/src/Index/AbstractAggregateIndex.php b/src/Index/AbstractAggregateIndex.php new file mode 100644 index 0000000..f8934c6 --- /dev/null +++ b/src/Index/AbstractAggregateIndex.php @@ -0,0 +1,66 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definition[] + */ + public function getDefinitions(): array + { + $defs = []; + foreach ($this->getIndexes() as $index) { + foreach ($index->getDefinitions() as $fqn => $def) { + $defs[$fqn] = $def; + } + } + return $defs; + } + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false) + { + foreach ($this->getIndexes() as $index) { + if ($def = $index->getDefinition($fqn, $globalFallback)) { + return $def; + } + } + } + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array + { + $refs = []; + foreach ($this->getIndexes() as $index) { + foreach ($index->getReferenceUris($fqn) as $ref) { + $refs[] = $ref; + } + } + return $refs; + } +} diff --git a/src/Index/DependenciesIndex.php b/src/Index/DependenciesIndex.php new file mode 100644 index 0000000..a355821 --- /dev/null +++ b/src/Index/DependenciesIndex.php @@ -0,0 +1,52 @@ +indexes; + } + + /** + * @param string $packageName + * @return Index + */ + public function getDependencyIndex(string $packageName): Index + { + if (!isset($this->indexes[$packageName])) { + $this->indexes[$packageName] = new Index; + } + return $this->indexes[$packageName]; + } + + /** + * @param string $packageName + * @return void + */ + public function removeDependencyIndex(string $packageName) + { + unset($this->indexes[$packageName]); + } + + /** + * @param string $packageName + * @return bool + */ + public function hasDependencyIndex(string $packageName): bool + { + return isset($this->indexes[$packageName]); + } +} diff --git a/src/Index/GlobalIndex.php b/src/Index/GlobalIndex.php new file mode 100644 index 0000000..e1e6d48 --- /dev/null +++ b/src/Index/GlobalIndex.php @@ -0,0 +1,38 @@ +stubsIndex = $stubsIndex; + $this->projectIndex = $projectIndex; + } + + /** + * @return ReadableIndex[] + */ + protected function getIndexes(): array + { + return [$this->stubsIndex, $this->projectIndex]; + } +} diff --git a/src/Index/Index.php b/src/Index/Index.php new file mode 100644 index 0000000..29cbb99 --- /dev/null +++ b/src/Index/Index.php @@ -0,0 +1,129 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definition[] + */ + public function getDefinitions(): array + { + return $this->definitions; + } + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false) + { + if (isset($this->definitions[$fqn])) { + return $this->definitions[$fqn]; + } + if ($globalFallback) { + $parts = explode('\\', $fqn); + $fqn = end($parts); + return $this->getDefinition($fqn); + } + } + + /** + * Registers a definition + * + * @param string $fqn The fully qualified name of the symbol + * @param string $definition The Definition object + * @return void + */ + public function setDefinition(string $fqn, Definition $definition) + { + $this->definitions[$fqn] = $definition; + } + + /** + * Unsets the Definition for a specific symbol + * and removes all references pointing to that symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return void + */ + public function removeDefinition(string $fqn) + { + unset($this->definitions[$fqn]); + unset($this->references[$fqn]); + } + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array + { + return $this->references[$fqn] ?? []; + } + + /** + * Adds a document URI as a referencee of a specific symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return void + */ + public function addReferenceUri(string $fqn, string $uri) + { + if (!isset($this->references[$fqn])) { + $this->references[$fqn] = []; + } + // TODO: use DS\Set instead of searching array + if (array_search($uri, $this->references[$fqn], true) === false) { + $this->references[$fqn][] = $uri; + } + } + + /** + * Removes a document URI as the container for a specific symbol + * + * @param string $fqn The fully qualified name of the symbol + * @param string $uri The URI + * @return void + */ + public function removeReferenceUri(string $fqn, string $uri) + { + if (!isset($this->references[$fqn])) { + return; + } + $index = array_search($fqn, $this->references[$fqn], true); + if ($index === false) { + return; + } + array_splice($this->references[$fqn], $index, 1); + } +} diff --git a/src/Index/ProjectIndex.php b/src/Index/ProjectIndex.php new file mode 100644 index 0000000..8b42f8f --- /dev/null +++ b/src/Index/ProjectIndex.php @@ -0,0 +1,51 @@ +sourceIndex = $sourceIndex; + $this->dependenciesIndex = $dependenciesIndex; + } + + /** + * @return ReadableIndex[] + */ + protected function getIndexes(): array + { + return [$this->sourceIndex, $this->dependenciesIndex]; + } + + /** + * @param string $uri + * @return Index + */ + public function getIndexForUri(string $uri): Index + { + if (preg_match('/\/vendor\/(\w+\/\w+)\//', $uri, $matches)) { + $packageName = $matches[0]; + return $this->dependenciesIndex->getDependencyIndex($packageName); + } + return $this->sourceIndex; + } +} diff --git a/src/Index/ReadableIndex.php b/src/Index/ReadableIndex.php new file mode 100644 index 0000000..40f4e40 --- /dev/null +++ b/src/Index/ReadableIndex.php @@ -0,0 +1,37 @@ + Definition] that maps fully qualified symbol names + * to Definitions + * + * @return Definitions[] + */ + public function getDefinitions(): array; + + /** + * Returns the Definition object by a specific FQN + * + * @param string $fqn + * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found + * @return Definition|null + */ + public function getDefinition(string $fqn, bool $globalFallback = false); + + /** + * Returns all URIs in this index that reference a symbol + * + * @param string $fqn The fully qualified name of the symbol + * @return string[] + */ + public function getReferenceUris(string $fqn): array; +} diff --git a/src/Index/StubsIndex.php b/src/Index/StubsIndex.php new file mode 100644 index 0000000..3828f31 --- /dev/null +++ b/src/Index/StubsIndex.php @@ -0,0 +1,27 @@ +getMessage(), + (string)$e, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, null, $e @@ -120,54 +133,74 @@ 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. - * @return InitializeResult + * @return Promise */ - public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): InitializeResult + public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise { - $this->rootPath = $rootPath; + return coroutine(function () use ($capabilities, $rootPath, $processId) { - if ($capabilities->xfilesProvider) { - $this->filesFinder = new ClientFilesFinder($this->client); - } else { - $this->filesFinder = new FileSystemFilesFinder; - } + if ($capabilities->xfilesProvider) { + $this->filesFinder = new ClientFilesFinder($this->client); + } else { + $this->filesFinder = new FileSystemFilesFinder; + } - if ($capabilities->xcontentProvider) { - $this->contentRetriever = new ClientContentRetriever($this->client); - } else { - $this->contentRetriever = new FileSystemContentRetriever; - } + if ($capabilities->xcontentProvider) { + $this->contentRetriever = new ClientContentRetriever($this->client); + } else { + $this->contentRetriever = new FileSystemContentRetriever; + } - $this->project = new Project($this->client, $this->contentRetriever); - $this->textDocument = new Server\TextDocument($this->project, $this->client); - $this->workspace = new Server\Workspace($this->project, $this->client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $stubsIndex = StubsIndex::read(); + $globalIndex = new GlobalIndex($stubsIndex, $projectIndex); - // start building project index - if ($rootPath !== null) { - $this->indexProject()->otherwise('\\LanguageServer\\crash'); - } + // The DefinitionResolver should look in stubs, the project source and dependencies + $definitionResolver = new DefinitionResolver($globalIndex); - $serverCapabilities = new ServerCapabilities(); - // Ask the client to return always full documents (because we need to rebuild the AST from scratch) - $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; - // Support "Find all symbols" - $serverCapabilities->documentSymbolProvider = true; - // Support "Find all symbols in workspace" - $serverCapabilities->workspaceSymbolProvider = true; - // Support "Format Code" - $serverCapabilities->documentFormattingProvider = true; - // Support "Go to definition" - $serverCapabilities->definitionProvider = true; - // Support "Find all references" - $serverCapabilities->referencesProvider = true; - // Support "Hover" - $serverCapabilities->hoverProvider = true; - // Support "Completion" - $serverCapabilities->completionProvider = new CompletionOptions; - $serverCapabilities->completionProvider->resolveProvider = false; - $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + $this->documentLoader = new PhpDocumentLoader( + $this->contentRetriever, + $projectIndex, + $definitionResolver + ); - return new InitializeResult($serverCapabilities); + if ($rootPath !== null) { + $pattern = Path::makeAbsolute('**/*.php', $rootPath); + $uris = yield $this->filesFinder->find($pattern); + $this->index($uris)->otherwise('\\LanguageServer\\crash'); + } + + $this->textDocument = new Server\TextDocument( + $this->documentLoader, + $definitionResolver, + $this->client, + $globalIndex + ); + // workspace/symbol should only look inside the project source and dependencies + $this->workspace = new Server\Workspace($projectIndex, $this->client); + + $serverCapabilities = new ServerCapabilities(); + // Ask the client to return always full documents (because we need to rebuild the AST from scratch) + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; + // Support "Find all symbols" + $serverCapabilities->documentSymbolProvider = true; + // Support "Find all symbols in workspace" + $serverCapabilities->workspaceSymbolProvider = true; + // Support "Format Code" + $serverCapabilities->documentFormattingProvider = true; + // Support "Go to definition" + $serverCapabilities->definitionProvider = true; + // Support "Find all references" + $serverCapabilities->referencesProvider = true; + // Support "Hover" + $serverCapabilities->hoverProvider = true; + // Support "Completion" + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + return new InitializeResult($serverCapabilities); + }); } /** @@ -193,20 +226,24 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher } /** - * Parses workspace files, one at a time. + * Will read and parse the passed source files in the project and add them to the appropiate indexes * * @return Promise */ - private function indexProject(): Promise + private function index(array $uris): Promise { - return coroutine(function () { - $pattern = Path::makeAbsolute('**/*.php', $this->rootPath); - $uris = yield $this->filesFinder->find($pattern); + return coroutine(function () use ($uris) { + $count = count($uris); $startTime = microtime(true); + // Parse PHP files foreach ($uris as $i => $uri) { + if ($this->documentLoader->isOpen($uri)) { + continue; + } + // Give LS to the chance to handle requests while indexing yield timeout(); $this->client->window->logMessage( @@ -214,7 +251,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher "Parsing file $i/$count: {$uri}" ); try { - yield $this->project->loadDocument($uri); + $document = yield $this->documentLoader->load($uri); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics()); + } } catch (ContentTooLargeException $e) { $this->client->window->logMessage( MessageType::INFO, diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 1e1a726..76009a9 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -10,34 +10,16 @@ use LanguageServer\NodeVisitor\{ DocBlockParser, DefinitionCollector, ColumnCalculator, - ReferencesCollector, - VariableReferencesCollector + ReferencesCollector }; +use LanguageServer\Index\Index; use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use PhpParser\NodeVisitor\NameResolver; use phpDocumentor\Reflection\DocBlockFactory; -use Sabre\Event\Promise; -use function Sabre\Event\coroutine; use Sabre\Uri; class PhpDocument { - /** - * The LanguageClient instance (to report errors etc) - * - * @var LanguageClient - */ - private $client; - - /** - * The Project this document belongs to (to register definitions etc) - * - * @var Project - */ - public $project; - // for whatever reason I get "cannot access private property" error if $project is not public - // https://github.com/felixfbecker/php-language-server/pull/49#issuecomment-252427359 - /** * The PHPParser instance * @@ -59,6 +41,11 @@ class PhpDocument */ private $definitionResolver; + /** + * @var Index + */ + private $index; + /** * The URI of the document * @@ -102,25 +89,30 @@ class PhpDocument private $referenceNodes; /** - * @param string $uri The URI of the document - * @param string $content The content of the document - * @param Project $project The Project this document belongs to (to register definitions etc) - * @param LanguageClient $client The LanguageClient instance (to report errors etc) - * @param Parser $parser The PHPParser instance - * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks + * Diagnostics for this document that were collected while parsing + * + * @var Diagnostic[] + */ + private $diagnostics; + + /** + * @param string $uri The URI of the document + * @param string $content The content of the document + * @param Index $index The Index to register definitions and references to + * @param Parser $parser The PHPParser instance + * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks + * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace */ public function __construct( string $uri, string $content, - Project $project, - LanguageClient $client, + Index $index, Parser $parser, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver ) { $this->uri = $uri; - $this->project = $project; - $this->client = $client; + $this->index = $index; $this->parser = $parser; $this->docBlockFactory = $docBlockFactory; $this->definitionResolver = $definitionResolver; @@ -154,9 +146,9 @@ class PhpDocument $errorHandler = new ErrorHandler\Collecting; $stmts = $this->parser->parse($content, $errorHandler); - $diagnostics = []; + $this->diagnostics = []; foreach ($errorHandler->getErrors() as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); } // $stmts can be null in case of a fatal parsing error @@ -180,7 +172,7 @@ class PhpDocument // Report errors from parsing docblocks foreach ($docBlockParser->errors as $error) { - $diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); + $this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php'); } $traverser = new NodeTraverser; @@ -198,34 +190,30 @@ class PhpDocument // Unregister old definitions if (isset($this->definitions)) { foreach ($this->definitions as $fqn => $definition) { - $this->project->removeDefinition($fqn); + $this->index->removeDefinition($fqn); } } // Register this document on the project for all the symbols defined in it $this->definitions = $definitionCollector->definitions; $this->definitionNodes = $definitionCollector->nodes; foreach ($definitionCollector->definitions as $fqn => $definition) { - $this->project->setDefinition($fqn, $definition); + $this->index->setDefinition($fqn, $definition); } // Unregister old references if (isset($this->referenceNodes)) { foreach ($this->referenceNodes as $fqn => $node) { - $this->project->removeReferenceUri($fqn, $this->uri); + $this->index->removeReferenceUri($fqn, $this->uri); } } // Register this document on the project for references $this->referenceNodes = $referencesCollector->nodes; foreach ($referencesCollector->nodes as $fqn => $nodes) { - $this->project->addReferenceUri($fqn, $this->uri); + $this->index->addReferenceUri($fqn, $this->uri); } $this->stmts = $stmts; } - - if (!$this->isVendored()) { - $this->client->textDocument->publishDiagnostics($this->uri, $diagnostics); - } } /** @@ -262,6 +250,16 @@ class PhpDocument return $this->content; } + /** + * Returns this document's diagnostics + * + * @return Diagnostic[] + */ + public function getDiagnostics() + { + return $this->diagnostics; + } + /** * Returns the URI of the document * @@ -357,57 +355,4 @@ class PhpDocument { return isset($this->definitions[$fqn]); } - - /** - * Returns the reference nodes for any node - * The references node MAY be in other documents, check the ownerDocument attribute - * - * @param Node $node - * @return Promise - */ - public function getReferenceNodesByNode(Node $node): Promise - { - return coroutine(function () use ($node) { - // Variables always stay in the boundary of the file and need to be searched inside their function scope - // by traversing the AST - if ( - $node instanceof Node\Expr\Variable - || $node instanceof Node\Param - || $node instanceof Node\Expr\ClosureUse - ) { - if ($node->name instanceof Node\Expr) { - return null; - } - // Find function/method/closure scope - $n = $node; - while (isset($n) && !($n instanceof Node\FunctionLike)) { - $n = $n->getAttribute('parentNode'); - } - if (!isset($n)) { - $n = $node->getAttribute('ownerDocument'); - } - $traverser = new NodeTraverser; - $refCollector = new VariableReferencesCollector($node->name); - $traverser->addVisitor($refCollector); - $traverser->traverse($n->getStmts()); - return $refCollector->nodes; - } - // Definition with a global FQN - $fqn = DefinitionResolver::getDefinedFqn($node); - if ($fqn === null) { - return []; - } - $refDocuments = yield $this->project->getReferenceDocuments($fqn); - $nodes = []; - foreach ($refDocuments as $document) { - $refs = $document->getReferenceNodesByFqn($fqn); - if ($refs !== null) { - foreach ($refs as $ref) { - $nodes[] = $ref; - } - } - } - return $nodes; - }); - } } diff --git a/src/PhpDocumentLoader.php b/src/PhpDocumentLoader.php new file mode 100644 index 0000000..b6c04cb --- /dev/null +++ b/src/PhpDocumentLoader.php @@ -0,0 +1,178 @@ + PhpDocument of open documents that should be kept in memory + * + * @var PhpDocument + */ + private $documents = []; + + /** + * @var ContentRetriever + */ + private $contentRetriever; + + /** + * @var ProjectIndex + */ + private $projectIndex; + + /** + * @var Parser + */ + private $parser; + + /** + * @var DocBlockFactory + */ + private $docBlockFactory; + + /** + * @var DefinitionResolver + */ + private $definitionResolver; + + /** + * @param ContentRetriever $contentRetriever + * @param ProjectIndex $project + * @param DefinitionResolver $definitionResolver + */ + public function __construct( + ContentRetriever $contentRetriever, + ProjectIndex $projectIndex, + DefinitionResolver $definitionResolver + ) { + $this->contentRetriever = $contentRetriever; + $this->projectIndex = $projectIndex; + $this->definitionResolver = $definitionResolver; + $this->parser = new Parser; + $this->docBlockFactory = DocBlockFactory::createInstance(); + } + + /** + * Returns the document indicated by uri. + * Returns null if the document if not loaded. + * + * @param string $uri + * @return PhpDocument|null + */ + public function get(string $uri) + { + return $this->documents[$uri] ?? null; + } + + /** + * Returns the document indicated by uri. + * If the document is not open, loads it. + * + * @param string $uri + * @return Promise + */ + public function getOrLoad(string $uri): Promise + { + return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->load($uri); + } + + /** + * Loads the document by doing a textDocument/xcontent request to the client. + * If the client does not support textDocument/xcontent, tries to read the file from the file system. + * The document is NOT added to the list of open documents, but definitions are registered. + * + * @param string $uri + * @return Promise + */ + public function load(string $uri): Promise + { + return coroutine(function () use ($uri) { + + $limit = 150000; + $content = yield $this->contentRetriever->retrieve($uri); + $size = strlen($content); + if ($size > $limit) { + throw new ContentTooLargeException($uri, $size, $limit); + } + + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = $this->create($uri, $content); + } + return $document; + }); + } + + /** + * Builds a PhpDocument instance + * + * @param string $uri + * @param string $content + * @return PhpDocument + */ + public function create(string $uri, string $content): PhpDocument + { + return new PhpDocument( + $uri, + $content, + $this->projectIndex->getIndexForUri($uri), + $this->parser, + $this->docBlockFactory, + $this->definitionResolver + ); + } + + /** + * Ensures a document is loaded and added to the list of open documents. + * + * @param string $uri + * @param string $content + * @return void + */ + public function open(string $uri, string $content) + { + if (isset($this->documents[$uri])) { + $document = $this->documents[$uri]; + $document->updateContent($content); + } else { + $document = $this->create($uri, $content); + $this->documents[$uri] = $document; + } + return $document; + } + + /** + * Removes the document with the specified URI from the list of open documents + * + * @param string $uri + * @return void + */ + public function close(string $uri) + { + unset($this->documents[$uri]); + } + + /** + * Returns true if the document is open (and loaded) + * + * @param string $uri + * @return bool + */ + public function isOpen(string $uri): bool + { + return isset($this->documents[$uri]); + } +} diff --git a/src/Project.php b/src/Project.php deleted file mode 100644 index 0582301..0000000 --- a/src/Project.php +++ /dev/null @@ -1,349 +0,0 @@ - PhpDocument] - * that maps URIs to loaded PhpDocuments - * - * @var PhpDocument[] - */ - private $documents = []; - - /** - * An associative array that maps fully qualified symbol names to Definitions - * - * @var Definition[] - */ - private $definitions = []; - - /** - * An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol - * - * @var PhpDocument[][] - */ - private $references = []; - - /** - * Instance of the PHP parser - * - * @var Parser - */ - private $parser; - - /** - * The DocBlockFactory instance to parse docblocks - * - * @var DocBlockFactory - */ - private $docBlockFactory; - - /** - * The DefinitionResolver instance to resolve reference nodes to Definitions - * - * @var DefinitionResolver - */ - private $definitionResolver; - - /** - * Reference to the language server client interface - * - * @var LanguageClient - */ - private $client; - - /** - * The content retriever - * - * @var ContentRetriever - */ - private $contentRetriever; - - public function __construct(LanguageClient $client, ContentRetriever $contentRetriever) - { - $this->client = $client; - $this->parser = new Parser; - $this->docBlockFactory = DocBlockFactory::createInstance(); - $this->definitionResolver = new DefinitionResolver($this); - $this->contentRetriever = $contentRetriever; - } - - /** - * Returns the document indicated by uri. - * Returns null if the document if not loaded. - * - * @param string $uri - * @return PhpDocument|null - */ - public function getDocument(string $uri) - { - return $this->documents[$uri] ?? null; - } - - /** - * Returns the document indicated by uri. - * If the document is not open, loads it. - * - * @param string $uri - * @return Promise - */ - public function getOrLoadDocument(string $uri) - { - return isset($this->documents[$uri]) ? Promise\resolve($this->documents[$uri]) : $this->loadDocument($uri); - } - - /** - * Loads the document by doing a textDocument/xcontent request to the client. - * If the client does not support textDocument/xcontent, tries to read the file from the file system. - * The document is NOT added to the list of open documents, but definitions are registered. - * - * @param string $uri - * @return Promise - */ - public function loadDocument(string $uri): Promise - { - return coroutine(function () use ($uri) { - $limit = 150000; - $content = yield $this->contentRetriever->retrieve($uri); - $size = strlen($content); - if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); - } - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); - } - return $document; - }); - } - - /** - * Ensures a document is loaded and added to the list of open documents. - * - * @param string $uri - * @param string $content - * @return void - */ - public function openDocument(string $uri, string $content) - { - if (isset($this->documents[$uri])) { - $document = $this->documents[$uri]; - $document->updateContent($content); - } else { - $document = new PhpDocument( - $uri, - $content, - $this, - $this->client, - $this->parser, - $this->docBlockFactory, - $this->definitionResolver - ); - $this->documents[$uri] = $document; - } - return $document; - } - - /** - * Removes the document with the specified URI from the list of open documents - * - * @param string $uri - * @return void - */ - public function closeDocument(string $uri) - { - unset($this->documents[$uri]); - } - - /** - * Returns true if the document is open (and loaded) - * - * @param string $uri - * @return bool - */ - public function isDocumentOpen(string $uri): bool - { - return isset($this->documents[$uri]); - } - - /** - * Returns an associative array [string => Definition] that maps fully qualified symbol names - * to Definitions - * - * @return Definitions[] - */ - public function getDefinitions() - { - return $this->definitions; - } - - /** - * Returns the Definition object by a specific FQN - * - * @param string $fqn - * @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found - * @return Definition|null - */ - public function getDefinition(string $fqn, $globalFallback = false) - { - if (isset($this->definitions[$fqn])) { - return $this->definitions[$fqn]; - } else if ($globalFallback) { - $parts = explode('\\', $fqn); - $fqn = end($parts); - return $this->getDefinition($fqn); - } - } - - /** - * Registers a definition - * - * @param string $fqn The fully qualified name of the symbol - * @param string $definition The Definition object - * @return void - */ - public function setDefinition(string $fqn, Definition $definition) - { - $this->definitions[$fqn] = $definition; - } - - /** - * Sets the Definition index - * - * @param Definition[] $definitions Map from FQN to Definition - * @return void - */ - public function setDefinitions(array $definitions) - { - $this->definitions = $definitions; - } - - /** - * Unsets the Definition for a specific symbol - * and removes all references pointing to that symbol - * - * @param string $fqn The fully qualified name of the symbol - * @return void - */ - public function removeDefinition(string $fqn) - { - unset($this->definitions[$fqn]); - unset($this->references[$fqn]); - } - - /** - * Adds a document URI as a referencee of a specific symbol - * - * @param string $fqn The fully qualified name of the symbol - * @return void - */ - public function addReferenceUri(string $fqn, string $uri) - { - if (!isset($this->references[$fqn])) { - $this->references[$fqn] = []; - } - // TODO: use DS\Set instead of searching array - if (array_search($uri, $this->references[$fqn], true) === false) { - $this->references[$fqn][] = $uri; - } - } - - /** - * Removes a document URI as the container for a specific symbol - * - * @param string $fqn The fully qualified name of the symbol - * @param string $uri The URI - * @return void - */ - public function removeReferenceUri(string $fqn, string $uri) - { - if (!isset($this->references[$fqn])) { - return; - } - $index = array_search($fqn, $this->references[$fqn], true); - if ($index === false) { - return; - } - array_splice($this->references[$fqn], $index, 1); - } - - /** - * Returns all documents that reference a symbol - * - * @param string $fqn The fully qualified name of the symbol - * @return Promise - */ - public function getReferenceDocuments(string $fqn): Promise - { - if (!isset($this->references[$fqn])) { - return Promise\resolve([]); - } - return Promise\all(array_map([$this, 'getOrLoadDocument'], $this->references[$fqn])); - } - - /** - * Returns an associative array [string => string[]] that maps fully qualified symbol names - * to URIs of the document where the symbol is referenced - * - * @return string[][] - */ - public function getReferenceUris() - { - return $this->references; - } - - /** - * Sets the reference index - * - * @param string[][] $references an associative array [string => string[]] from FQN to URIs - * @return void - */ - public function setReferenceUris(array $references) - { - $this->references = $references; - } - - /** - * Returns the document where a symbol is defined - * - * @param string $fqn The fully qualified name of the symbol - * @return Promise - */ - public function getDefinitionDocument(string $fqn): Promise - { - if (!isset($this->definitions[$fqn])) { - return Promise\resolve(null); - } - return $this->getOrLoadDocument($this->definitions[$fqn]->symbolInformation->location->uri); - } - - /** - * Returns true if the given FQN is defined in the project - * - * @param string $fqn The fully qualified name of the symbol - * @return bool - */ - public function isDefined(string $fqn): bool - { - return isset($this->definitions[$fqn]); - } -} diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5998acc..84103df 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -3,9 +3,10 @@ declare(strict_types = 1); namespace LanguageServer\Server; -use LanguageServer\{LanguageClient, Project, PhpDocument, DefinitionResolver, CompletionProvider}; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; -use PhpParser\Node; +use PhpParser\{Node, NodeTraverser}; +use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider}; +use LanguageServer\NodeVisitor\VariableReferencesCollector; use LanguageServer\Protocol\{ TextDocumentItem, TextDocumentIdentifier, @@ -23,7 +24,9 @@ use LanguageServer\Protocol\{ CompletionItem, CompletionItemKind }; +use LanguageServer\Index\ReadableIndex; use Sabre\Event\Promise; +use Sabre\Uri; use function Sabre\Event\coroutine; /** @@ -58,13 +61,29 @@ class TextDocument */ private $completionProvider; - public function __construct(Project $project, LanguageClient $client) - { - $this->project = $project; + /** + * @var ReadableIndex + */ + private $index; + + /** + * @param PhpDocumentLoader $documentLoader + * @param DefinitionResolver $definitionResolver + * @param LanguageClient $client + * @param ReadableIndex $index + */ + public function __construct( + PhpDocumentLoader $documentLoader, + DefinitionResolver $definitionResolver, + LanguageClient $client, + ReadableIndex $index + ) { + $this->documentLoader = $documentLoader; $this->client = $client; $this->prettyPrinter = new PrettyPrinter(); - $this->definitionResolver = new DefinitionResolver($project); - $this->completionProvider = new CompletionProvider($this->definitionResolver, $project); + $this->definitionResolver = $definitionResolver; + $this->completionProvider = new CompletionProvider($this->definitionResolver, $index); + $this->index = $index; } /** @@ -76,7 +95,7 @@ class TextDocument */ public function documentSymbol(TextDocumentIdentifier $textDocument): Promise { - return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { + return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) { $symbols = []; foreach ($document->getDefinitions() as $fqn => $definition) { $symbols[] = $definition->symbolInformation; @@ -95,7 +114,10 @@ class TextDocument */ public function didOpen(TextDocumentItem $textDocument) { - $this->project->openDocument($textDocument->uri, $textDocument->text); + $document = $this->documentLoader->open($textDocument->uri, $textDocument->text); + if (!$document->isVendored()) { + $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); + } } /** @@ -107,7 +129,9 @@ class TextDocument */ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $contentChanges) { - $this->project->getDocument($textDocument->uri)->updateContent($contentChanges[0]->text); + $document = $this->documentLoader->get($textDocument->uri); + $document->updateContent($contentChanges[0]->text); + $this->client->textDocument->publishDiagnostics($textDocument->uri, $document->getDiagnostics()); } /** @@ -120,7 +144,7 @@ class TextDocument */ public function didClose(TextDocumentIdentifier $textDocument) { - $this->project->closeDocument($textDocument->uri); + $this->documentLoader->close($textDocument->uri); } /** @@ -132,7 +156,7 @@ class TextDocument */ public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options) { - return $this->project->getOrLoadDocument($textDocument->uri)->then(function (PhpDocument $document) { + return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) { return $document->getFormattedText(); }); } @@ -150,15 +174,55 @@ class TextDocument Position $position ): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; } - $refs = yield $document->getReferenceNodesByNode($node); $locations = []; - foreach ($refs as $ref) { - $locations[] = Location::fromNode($ref); + // Variables always stay in the boundary of the file and need to be searched inside their function scope + // by traversing the AST + if ( + $node instanceof Node\Expr\Variable + || $node instanceof Node\Param + || $node instanceof Node\Expr\ClosureUse + ) { + if ($node->name instanceof Node\Expr) { + return null; + } + // Find function/method/closure scope + $n = $node; + while (isset($n) && !($n instanceof Node\FunctionLike)) { + $n = $n->getAttribute('parentNode'); + } + if (!isset($n)) { + $n = $node->getAttribute('ownerDocument'); + } + $traverser = new NodeTraverser; + $refCollector = new VariableReferencesCollector($node->name); + $traverser->addVisitor($refCollector); + $traverser->traverse($n->getStmts()); + foreach ($refCollector->nodes as $ref) { + $locations[] = Location::fromNode($ref); + } + } else { + // Definition with a global FQN + $fqn = DefinitionResolver::getDefinedFqn($node); + if ($fqn === null) { + return []; + } + $refDocuments = yield Promise\all(array_map( + [$this->documentLoader, 'getOrLoad'], + $this->index->getReferenceUris($fqn) + )); + foreach ($refDocuments as $document) { + $refs = $document->getReferenceNodesByFqn($fqn); + if ($refs !== null) { + foreach ($refs as $ref) { + $locations[] = Location::fromNode($ref); + } + } + } } return $locations; }); @@ -175,13 +239,17 @@ class TextDocument public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); $node = $document->getNodeAtPosition($position); if ($node === null) { return []; } $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); - if ($def === null || $def->symbolInformation === null) { + if ( + $def === null + || $def->symbolInformation === null + || Uri\parse($def->symbolInformation->location->uri)['scheme'] === 'phpstubs' + ) { return []; } return $def->symbolInformation->location; @@ -198,7 +266,7 @@ class TextDocument public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); // Find the node under the cursor $node = $document->getNodeAtPosition($position); if ($node === null) { @@ -237,7 +305,7 @@ class TextDocument public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise { return coroutine(function () use ($textDocument, $position) { - $document = yield $this->project->getOrLoadDocument($textDocument->uri); + $document = yield $this->documentLoader->getOrLoad($textDocument->uri); return $this->completionProvider->provideCompletion($document, $position); }); } diff --git a/src/Server/Workspace.php b/src/Server/Workspace.php index 26feb72..66f8606 100644 --- a/src/Server/Workspace.php +++ b/src/Server/Workspace.php @@ -4,6 +4,7 @@ declare(strict_types = 1); namespace LanguageServer\Server; use LanguageServer\{LanguageClient, Project}; +use LanguageServer\Index\ProjectIndex; use LanguageServer\Protocol\SymbolInformation; /** @@ -19,15 +20,18 @@ class Workspace private $client; /** - * The current project database + * The symbol index for the workspace * - * @var Project + * @var ProjectIndex */ - private $project; + private $index; - public function __construct(Project $project, LanguageClient $client) + /** + * @param ProjectIndex $index Index that is searched on a workspace/symbol request + */ + public function __construct(ProjectIndex $index, LanguageClient $client) { - $this->project = $project; + $this->index = $index; $this->client = $client; } @@ -40,7 +44,7 @@ class Workspace public function symbol(string $query): array { $symbols = []; - foreach ($this->project->getDefinitions() as $fqn => $definition) { + foreach ($this->index->getDefinitions() as $fqn => $definition) { if ($query === '' || stripos($fqn, $query) !== false) { $symbols[] = $definition->symbolInformation; } diff --git a/tests/LanguageServerTest.php b/tests/LanguageServerTest.php index 8bd7b4f..d9f0d81 100644 --- a/tests/LanguageServerTest.php +++ b/tests/LanguageServerTest.php @@ -6,7 +6,16 @@ namespace LanguageServer\Tests; use PHPUnit\Framework\TestCase; use LanguageServer\LanguageServer; use LanguageServer\Protocol\{ - Message, ClientCapabilities, TextDocumentSyncKind, MessageType, TextDocumentItem, TextDocumentIdentifier}; + Message, + ClientCapabilities, + TextDocumentSyncKind, + MessageType, + TextDocumentItem, + TextDocumentIdentifier, + InitializeResult, + ServerCapabilities, + CompletionOptions +}; use AdvancedJsonRpc; use Webmozart\Glob\Glob; use Webmozart\PathUtil\Path; @@ -18,41 +27,22 @@ class LanguageServerTest extends TestCase { public function testInitialize() { - $reader = new MockProtocolStream(); - $writer = new MockProtocolStream(); - $server = new LanguageServer($reader, $writer); - $promise = new Promise; - $writer->once('message', [$promise, 'fulfill']); - $reader->write(new Message(new AdvancedJsonRpc\Request(1, 'initialize', [ - 'rootPath' => __DIR__, - 'processId' => getmypid(), - 'capabilities' => new ClientCapabilities() - ]))); - $msg = $promise->wait(); - $this->assertNotNull($msg, 'message event should be emitted'); - $this->assertInstanceOf(AdvancedJsonRpc\SuccessResponse::class, $msg->body); - $this->assertEquals((object)[ - 'capabilities' => (object)[ - 'textDocumentSync' => TextDocumentSyncKind::FULL, - 'documentSymbolProvider' => true, - 'hoverProvider' => true, - 'completionProvider' => (object)[ - 'resolveProvider' => false, - 'triggerCharacters' => ['$', '>'] - ], - 'signatureHelpProvider' => null, - 'definitionProvider' => true, - 'referencesProvider' => true, - 'documentHighlightProvider' => null, - 'workspaceSymbolProvider' => true, - 'codeActionProvider' => null, - 'codeLensProvider' => null, - 'documentFormattingProvider' => true, - 'documentRangeFormattingProvider' => null, - 'documentOnTypeFormattingProvider' => null, - 'renameProvider' => null - ] - ], $msg->body->result); + $server = new LanguageServer(new MockProtocolStream, new MockProtocolStream); + $result = $server->initialize(new ClientCapabilities, __DIR__, getmypid())->wait(); + + $serverCapabilities = new ServerCapabilities(); + $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; + $serverCapabilities->documentSymbolProvider = true; + $serverCapabilities->workspaceSymbolProvider = true; + $serverCapabilities->documentFormattingProvider = true; + $serverCapabilities->definitionProvider = true; + $serverCapabilities->referencesProvider = true; + $serverCapabilities->hoverProvider = true; + $serverCapabilities->completionProvider = new CompletionOptions; + $serverCapabilities->completionProvider->resolveProvider = false; + $serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; + + $this->assertEquals(new InitializeResult($serverCapabilities), $result); } public function testIndexingWithDirectFileAccess() diff --git a/tests/NodeVisitor/DefinitionCollectorTest.php b/tests/NodeVisitor/DefinitionCollectorTest.php index 800373d..9b60814 100644 --- a/tests/NodeVisitor/DefinitionCollectorTest.php +++ b/tests/NodeVisitor/DefinitionCollectorTest.php @@ -6,9 +6,11 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use PhpParser\{NodeTraverser, Node}; use PhpParser\NodeVisitor\NameResolver; -use LanguageServer\{LanguageClient, Project, PhpDocument, Parser, DefinitionResolver}; +use phpDocumentor\Reflection\DocBlockFactory; +use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\ClientCapabilities; +use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex}; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector}; use function LanguageServer\pathToUri; @@ -17,19 +19,25 @@ class DefinitionCollectorTest extends TestCase { public function testCollectsSymbols() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); + $path = realpath(__DIR__ . '/../../fixtures/symbols.php'); + $uri = pathToUri($path); $parser = new Parser; - $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/symbols.php')); - $document = $project->loadDocument($uri)->wait(); + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + $content = file_get_contents($path); + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + $stmts = $parser->parse($content); + $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); + $definitionCollector = new DefinitionCollector($definitionResolver); $traverser->addVisitor($definitionCollector); - $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); + $defNodes = $definitionCollector->nodes; + $this->assertEquals([ 'TestNamespace', 'TestNamespace\\TEST_CONST', @@ -57,19 +65,25 @@ class DefinitionCollectorTest extends TestCase public function testDoesNotCollectReferences() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); + $path = realpath(__DIR__ . '/../../fixtures/references.php'); + $uri = pathToUri($path); $parser = new Parser; - $uri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); - $document = $project->loadDocument($uri)->wait(); + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + $content = file_get_contents($path); + $document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); + $stmts = $parser->parse($content); + $traverser = new NodeTraverser; $traverser->addVisitor(new NameResolver); $traverser->addVisitor(new ReferencesAdder($document)); - $definitionCollector = new DefinitionCollector(new DefinitionResolver($project)); + $definitionCollector = new DefinitionCollector($definitionResolver); $traverser->addVisitor($definitionCollector); - $stmts = $parser->parse(file_get_contents($uri)); $traverser->traverse($stmts); + $defNodes = $definitionCollector->nodes; + $this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes)); $this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']); $this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']); diff --git a/tests/ProjectTest.php b/tests/PhpDocumentLoaderTest.php similarity index 50% rename from tests/ProjectTest.php rename to tests/PhpDocumentLoaderTest.php index 6fef176..7be062d 100644 --- a/tests/ProjectTest.php +++ b/tests/PhpDocumentLoaderTest.php @@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument}; +use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Protocol\{ TextDocumentItem, TextDocumentIdentifier, @@ -18,31 +19,35 @@ use LanguageServer\Protocol\{ use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use function LanguageServer\pathToUri; -class ProjectTest extends TestCase +class PhpDocumentLoaderTest extends TestCase { /** - * @var Project $project + * @var PhpDocumentLoader */ - private $project; + private $loader; public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $this->loader = new PhpDocumentLoader( + new FileSystemContentRetriever, + $projectIndex, + new DefinitionResolver($projectIndex) + ); } - public function testGetOrLoadDocumentLoadsDocument() + public function testGetOrLoadLoadsDocument() { - $document = $this->project->getOrLoadDocument(pathToUri(__FILE__))->wait(); + $document = $this->loader->getOrLoad(pathToUri(__FILE__))->wait(); $this->assertNotNull($document); $this->assertInstanceOf(PhpDocument::class, $document); } - public function testGetDocumentReturnsOpenedInstance() + public function testGetReturnsOpenedInstance() { - $document1 = $this->project->openDocument(pathToUri(__FILE__), file_get_contents(__FILE__)); - $document2 = $this->project->getDocument(pathToUri(__FILE__)); + $document1 = $this->loader->open(pathToUri(__FILE__), file_get_contents(__FILE__)); + $document2 = $this->loader->get(pathToUri(__FILE__)); $this->assertSame($document1, $document2); } diff --git a/tests/PhpDocumentTest.php b/tests/PhpDocumentTest.php index a4e8ffa..b9b3704 100644 --- a/tests/PhpDocumentTest.php +++ b/tests/PhpDocumentTest.php @@ -4,36 +4,36 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; +use phpDocumentor\Reflection\DocBlockFactory; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{LanguageClient, Project}; +use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser}; use LanguageServer\NodeVisitor\NodeAtPositionFinder; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use PhpParser\Node; class PhpDocumentTest extends TestCase { - /** - * @var Project $project - */ - private $project; - - public function setUp() + public function createDocument(string $uri, string $content) { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); + $parser = new Parser; + $docBlockFactory = DocBlockFactory::createInstance(); + $index = new Index; + $definitionResolver = new DefinitionResolver($index); + return new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); } public function testParsesVariableVariables() { - $document = $this->project->openDocument('whatever', "createDocument('whatever', "assertEquals([], $document->getDefinitions()); } public function testGetNodeAtPosition() { - $document = $this->project->openDocument('whatever', "createDocument('whatever', "getNodeAtPosition(new Position(1, 13)); $this->assertInstanceOf(Node\Name\FullyQualified::class, $node); $this->assertEquals('SomeClass', (string)$node); @@ -41,19 +41,19 @@ class PhpDocumentTest extends TestCase public function testIsVendored() { - $document = $this->project->openDocument('file:///dir/vendor/x.php', "createDocument('file:///dir/vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///c:/dir/vendor/x.php', "createDocument('file:///c:/dir/vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///vendor/x.php', "createDocument('file:///vendor/x.php', "assertEquals(true, $document->isVendored()); - $document = $this->project->openDocument('file:///dir/vendor.php', "createDocument('file:///dir/vendor.php', "assertEquals(false, $document->isVendored()); - $document = $this->project->openDocument('file:///dir/x.php', "createDocument('file:///dir/x.php', "assertEquals(false, $document->isVendored()); } } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 38e2c8a..602bda0 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use function LanguageServer\pathToUri; @@ -24,9 +25,9 @@ abstract class ServerTestCase extends TestCase protected $workspace; /** - * @var Project + * @var PhpDocumentLoader */ - protected $project; + protected $documentLoader; /** * Map from FQN to Location of definition @@ -44,10 +45,13 @@ abstract class ServerTestCase extends TestCase public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($this->project, $client); - $this->workspace = new Server\Workspace($this->project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + + $definitionResolver = new DefinitionResolver($projectIndex); + $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($projectIndex, $client); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); @@ -55,11 +59,11 @@ abstract class ServerTestCase extends TestCase $referencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/references.php')); $useUri = pathToUri(realpath(__DIR__ . '/../../fixtures/use.php')); - $this->project->loadDocument($symbolsUri)->wait(); - $this->project->loadDocument($referencesUri)->wait(); - $this->project->loadDocument($globalSymbolsUri)->wait(); - $this->project->loadDocument($globalReferencesUri)->wait(); - $this->project->loadDocument($useUri)->wait(); + $this->documentLoader->load($symbolsUri)->wait(); + $this->documentLoader->load($referencesUri)->wait(); + $this->documentLoader->load($globalSymbolsUri)->wait(); + $this->documentLoader->load($globalReferencesUri)->wait(); + $this->documentLoader->load($useUri)->wait(); // @codingStandardsIgnoreStart $this->definitionLocations = [ diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index 587f171..1e48d7b 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project, CompletionProvider}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, @@ -27,23 +28,26 @@ class CompletionTest extends TestCase private $textDocument; /** - * @var Project + * @var PhpDocumentLoader */ - private $project; + private $loader; public function setUp() { $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $this->project = new Project($client, new FileSystemContentRetriever); - $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); - $this->project->loadDocument(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); - $this->textDocument = new Server\TextDocument($this->project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $contentRetriever = new FileSystemContentRetriever; + $this->loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/global_symbols.php'))->wait(); + $this->loader->load(pathToUri(__DIR__ . '/../../../fixtures/symbols.php'))->wait(); + $this->textDocument = new Server\TextDocument($this->loader, $definitionResolver, $client, $projectIndex); } public function testPropertyAndMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(3, 7) @@ -67,7 +71,7 @@ class CompletionTest extends TestCase public function testPropertyAndMethodWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(3, 6) @@ -91,7 +95,7 @@ class CompletionTest extends TestCase public function testVariable() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(8, 5) @@ -123,7 +127,7 @@ class CompletionTest extends TestCase public function testVariableWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/variable_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(8, 6) @@ -145,7 +149,7 @@ class CompletionTest extends TestCase public function testNewInNamespace() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_new.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 10) @@ -177,7 +181,7 @@ class CompletionTest extends TestCase public function testUsedClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/used_class.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 5) @@ -195,7 +199,7 @@ class CompletionTest extends TestCase public function testStaticPropertyWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_property_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 14) @@ -216,7 +220,7 @@ class CompletionTest extends TestCase public function testStaticWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 11) @@ -249,7 +253,7 @@ class CompletionTest extends TestCase public function testStaticMethodWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 13) @@ -282,7 +286,7 @@ class CompletionTest extends TestCase public function testClassConstWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/class_const_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 13) @@ -315,7 +319,7 @@ class CompletionTest extends TestCase public function testFullyQualifiedClass() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/fully_qualified_class.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(6, 6) @@ -336,7 +340,7 @@ class CompletionTest extends TestCase public function testKeywords() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/keywords.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(2, 1) @@ -350,7 +354,7 @@ class CompletionTest extends TestCase public function testHtmlWithoutPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(0, 0) @@ -372,7 +376,7 @@ class CompletionTest extends TestCase public function testHtmlWithPrefix() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(0, 1) @@ -394,7 +398,7 @@ class CompletionTest extends TestCase public function testNamespace() { $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/namespace.php'); - $this->project->openDocument($completionUri, file_get_contents($completionUri)); + $this->loader->open($completionUri, file_get_contents($completionUri)); $items = $this->textDocument->completion( new TextDocumentIdentifier($completionUri), new Position(4, 6) diff --git a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php index 20ea70b..4c09e18 100644 --- a/tests/Server/TextDocument/Definition/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/Definition/GlobalFallbackTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\Server\ServerTestCase; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use Sabre\Event\Promise; @@ -14,11 +15,14 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $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')); + $definitionResolver = new DefinitionResolver($projectIndex); + $contentRetriever = new FileSystemContentRetriever; + $loader = new PhpDocumentLoader($contentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $loader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); + $loader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); } public function testClassDoesNotFallback() diff --git a/tests/Server/TextDocument/DidChangeTest.php b/tests/Server/TextDocument/DidChangeTest.php index 9df301a..bdd3b22 100644 --- a/tests/Server/TextDocument/DidChangeTest.php +++ b/tests/Server/TextDocument/DidChangeTest.php @@ -5,8 +5,9 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Protocol\{ TextDocumentIdentifier, TextDocumentItem, @@ -21,10 +22,12 @@ class DidChangeTest extends TestCase { public function test() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $textDocument = new Server\TextDocument($project, $client); - $phpDocument = $project->openDocument('whatever', "open('whatever', "openDocument('whatever', 'hello world'); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $phpDocument = $loader->open('whatever', "uri = 'whatever'; @@ -28,6 +31,6 @@ class DidCloseTest extends TestCase $textDocument->didClose(new TextDocumentIdentifier($textDocumentItem->uri)); - $this->assertFalse($project->isDocumentOpen($textDocumentItem->uri)); + $this->assertFalse($loader->isOpen($textDocumentItem->uri)); } } diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index 32aee22..abb3d6d 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project}; +use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{ TextDocumentIdentifier, @@ -20,23 +21,14 @@ use function LanguageServer\{pathToUri, uriToPath}; class FormattingTest extends TestCase { - /** - * @var Server\TextDocument - */ - private $textDocument; - - public function setUp() - { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); - } - public function testFormatting() { + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $textDocument = new Server\TextDocument($project, $client); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); + $path = realpath(__DIR__ . '/../../../fixtures/format.php'); $uri = pathToUri($path); diff --git a/tests/Server/TextDocument/ParseErrorsTest.php b/tests/Server/TextDocument/ParseErrorsTest.php index af2bdd8..6c927f8 100644 --- a/tests/Server/TextDocument/ParseErrorsTest.php +++ b/tests/Server/TextDocument/ParseErrorsTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, Client, LanguageClient, Project, ClientHandler}; +use LanguageServer\{Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities}; use Sabre\Event\Promise; @@ -36,8 +37,10 @@ class ParseErrorsTest extends TestCase return Promise\resolve(null); } }; - $project = new Project($client, new FileSystemContentRetriever); - $this->textDocument = new Server\TextDocument($project, $client); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $loader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); + $this->textDocument = new Server\TextDocument($loader, $definitionResolver, $client, $projectIndex); } private function openFile($file) diff --git a/tests/Server/TextDocument/References/GlobalFallbackTest.php b/tests/Server/TextDocument/References/GlobalFallbackTest.php index 4e6d07a..2679d15 100644 --- a/tests/Server/TextDocument/References/GlobalFallbackTest.php +++ b/tests/Server/TextDocument/References/GlobalFallbackTest.php @@ -5,7 +5,8 @@ namespace LanguageServer\Tests\Server\TextDocument\References; use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; -use LanguageServer\{Server, LanguageClient, Project}; +use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; +use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities}; use LanguageServer\Tests\Server\ServerTestCase; @@ -14,11 +15,13 @@ class GlobalFallbackTest extends ServerTestCase { public function setUp() { - $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); - $project = new Project($client, new FileSystemContentRetriever); - $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')); + $projectIndex = new ProjectIndex(new Index, new DependenciesIndex); + $definitionResolver = new DefinitionResolver($projectIndex); + $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->documentLoader->open('global_fallback', file_get_contents(__DIR__ . '/../../../../fixtures/global_fallback.php')); + $this->documentLoader->open('global_symbols', file_get_contents(__DIR__ . '/../../../../fixtures/global_symbols.php')); } public function testClassDoesNotFallback()