├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── COPYING ├── README.md ├── bin └── tenkawa.php ├── composer.json ├── composer.lock ├── images └── tenkawa-logo.png ├── phpstan.neon ├── phpunit.xml.dist ├── src └── Tsufeki │ └── Tenkawa │ ├── BeberleiAssert │ └── BeberleiAssertPlugin.php │ ├── Doctrine │ └── DoctrinePlugin.php │ ├── Mockery │ └── MockeryPlugin.php │ ├── Phony │ └── PhonyPlugin.php │ ├── Php │ ├── Composer │ │ ├── ComposerFileFilter.php │ │ └── ComposerService.php │ ├── Feature │ │ ├── Completion │ │ │ ├── GlobalSymbolCompleter.php │ │ │ ├── ImportSymbolCompleter.php │ │ │ ├── MemberSymbolCompleter.php │ │ │ ├── SymbolCompleter.php │ │ │ ├── SymbolCompletionProvider.php │ │ │ ├── VariableCompletionProvider.php │ │ │ ├── VariableGatheringVisitor.php │ │ │ └── WholeFileSnippetCompletionProvider.php │ │ ├── DocumentSymbols │ │ │ └── SymbolDocumentSymbolsProvider.php │ │ ├── GoToDefinition │ │ │ ├── ParentMemberGoToDefinitionProvider.php │ │ │ └── SymbolGoToDefinitionProvider.php │ │ ├── GoToImplementation │ │ │ ├── ClassLikeGoToImplementationProvider.php │ │ │ ├── FindClassLikeImplementationsVisitor.php │ │ │ ├── FindMemberImplementationsVisitor.php │ │ │ └── MemberGoToImplementationProvider.php │ │ ├── Hover │ │ │ ├── ExpressionTypeHoverProvider.php │ │ │ ├── HoverFormatter.php │ │ │ └── SymbolHoverProvider.php │ │ ├── Refactoring │ │ │ ├── Differ.php │ │ │ ├── EditHelper.php │ │ │ ├── FineDiffer.php │ │ │ ├── FixAutoloadClassNameRefactoring.php │ │ │ ├── ImportCodeActionProvider.php │ │ │ ├── ImportEditData.php │ │ │ ├── Importer.php │ │ │ ├── Indent.php │ │ │ ├── RefactoringExecutor.php │ │ │ └── WorkspaceEditCommandProvider.php │ │ ├── References │ │ │ ├── FindInheritedMembersVisitor.php │ │ │ ├── GlobalReferenceFinder.php │ │ │ ├── GlobalReferencesIndexDataProvider.php │ │ │ ├── MemberReferenceFinder.php │ │ │ ├── Reference.php │ │ │ ├── ReferenceFinder.php │ │ │ └── SymbolReferencesProvider.php │ │ ├── SignatureHelp │ │ │ ├── ReflectionSignatureFinder.php │ │ │ ├── SignatureFinder.php │ │ │ └── SymbolSignatureHelpProvider.php │ │ └── WorkspaceSymbols │ │ │ └── ReflectionWorkspaceSymbolsProvider.php │ ├── Index │ │ └── StubsIndexer.php │ ├── NodeFinder │ │ ├── NameContextTaggingVisitor.php │ │ └── NodeFinder.php │ ├── Parser │ │ ├── Ast.php │ │ ├── FindIntersectingNodesVisitor.php │ │ ├── FindNodeVisitor.php │ │ ├── Parser.php │ │ ├── ParserDiagnosticsProvider.php │ │ ├── PhpParserAdapter.php │ │ └── TokenIterator.php │ ├── PhpDoc │ │ └── PhpDocFormatter.php │ ├── PhpPlugin.php │ ├── PhpStan │ │ ├── Analyser │ │ │ ├── AnalysedCacheAware.php │ │ │ ├── AnalysedDocumentAware.php │ │ │ ├── AnalysedProjectAware.php │ │ │ └── Analyser.php │ │ ├── IndexReflection │ │ │ ├── DummyFunctionReflectionFactory.php │ │ │ ├── DummyReflectionClass.php │ │ │ ├── DummyReflectionMethod.php │ │ │ ├── DummyReflectionProperty.php │ │ │ ├── DummyReflectionType.php │ │ │ ├── IndexBroker.php │ │ │ ├── IndexClassConstantReflection.php │ │ │ ├── IndexClassReflection.php │ │ │ ├── IndexFunctionReflection.php │ │ │ ├── IndexMethodReflection.php │ │ │ ├── IndexParameterReflection.php │ │ │ ├── IndexPropertyReflection.php │ │ │ └── SignatureVariantFactory.php │ │ ├── PhpDocResolver │ │ │ ├── PhpDocResolver.php │ │ │ └── PhpDocResolverVisitor.php │ │ ├── PhpStanDiagnosticsProvider.php │ │ ├── PhpStanSignatureFinder.php │ │ ├── PhpStanTypeInference.php │ │ └── Utils │ │ │ ├── AstPruner.php │ │ │ ├── DocumentParser.php │ │ │ ├── ErrorTolerantPrettyPrinter.php │ │ │ └── patch.php │ ├── Reflection │ │ ├── ClassResolver.php │ │ ├── ClassResolverExtension.php │ │ ├── ConstExprEvaluation.php │ │ ├── ConstExprEvaluator.php │ │ ├── Element │ │ │ ├── ClassConst.php │ │ │ ├── ClassLike.php │ │ │ ├── Const_.php │ │ │ ├── DocComment.php │ │ │ ├── Element.php │ │ │ ├── Function_.php │ │ │ ├── MemberTrait.php │ │ │ ├── Method.php │ │ │ ├── Param.php │ │ │ ├── Property.php │ │ │ ├── TraitAlias.php │ │ │ ├── TraitInsteadOf.php │ │ │ ├── Type.php │ │ │ └── Variable.php │ │ ├── IndexReflectionProvider.php │ │ ├── InheritPhpDocClassResolverExtension.php │ │ ├── InheritanceTreeTraverser.php │ │ ├── InheritanceTreeVisitor.php │ │ ├── MembersFromAnnotationClassResolverExtension.php │ │ ├── NameContext.php │ │ ├── NameContextVisitor.php │ │ ├── NameHelper.php │ │ ├── ReflectionIndexDataProvider.php │ │ ├── ReflectionProvider.php │ │ ├── ReflectionTransformer.php │ │ ├── ReflectionVisitor.php │ │ ├── Resolved │ │ │ ├── ResolvedClassConst.php │ │ │ ├── ResolvedClassLike.php │ │ │ ├── ResolvedMemberTrait.php │ │ │ ├── ResolvedMethod.php │ │ │ └── ResolvedProperty.php │ │ └── StubsReflectionTransformer.php │ ├── Symbol │ │ ├── DefinitionSymbol.php │ │ ├── DefinitionSymbolExtractor.php │ │ ├── DocCommentSymbolExtractor.php │ │ ├── GlobalSymbol.php │ │ ├── GlobalSymbolExtractor.php │ │ ├── MemberSymbol.php │ │ ├── MemberSymbolExtractor.php │ │ ├── NodePathSymbolExtractor.php │ │ ├── Symbol.php │ │ ├── SymbolExtractor.php │ │ └── SymbolReflection.php │ └── TypeInference │ │ ├── BasicType.php │ │ ├── IntersectionType.php │ │ ├── ObjectType.php │ │ ├── Type.php │ │ ├── TypeInference.php │ │ └── UnionType.php │ ├── PhpUnit │ ├── MockBuilderDynamicReturnTypeExtension.php │ └── PhpUnitPlugin.php │ ├── Prophecy │ └── ProphecyPlugin.php │ ├── Server │ ├── Document │ │ ├── Document.php │ │ ├── DocumentStore.php │ │ └── Project.php │ ├── Event │ │ ├── Document │ │ │ ├── OnChange.php │ │ │ ├── OnClose.php │ │ │ ├── OnOpen.php │ │ │ ├── OnProjectClose.php │ │ │ └── OnProjectOpen.php │ │ ├── EventDispatcher.php │ │ ├── OnFileChange.php │ │ ├── OnIndexingFinished.php │ │ ├── OnInit.php │ │ ├── OnShutdown.php │ │ └── OnStart.php │ ├── Exception │ │ ├── CancelledException.php │ │ ├── DocumentNotOpenException.php │ │ ├── IoException.php │ │ ├── ProjectNotOpenException.php │ │ ├── RequestCancelledException.php │ │ ├── TransportException.php │ │ ├── UnknownCommandException.php │ │ └── UriException.php │ ├── Feature │ │ ├── Capabilities │ │ │ ├── ClientCapabilities.php │ │ │ ├── CompletionOptions.php │ │ │ ├── DocumentSymbolClientCapabilities.php │ │ │ ├── DynamicRegistrationCapability.php │ │ │ ├── ExecuteCommandOptions.php │ │ │ ├── GoToClientCapabilities.php │ │ │ ├── InitializeResult.php │ │ │ ├── SaveOptions.php │ │ │ ├── ServerCapabilities.php │ │ │ ├── SignatureHelpOptions.php │ │ │ ├── TextDocumentClientCapabilities.php │ │ │ ├── TextDocumentSyncKind.php │ │ │ ├── TextDocumentSyncOptions.php │ │ │ ├── WorkspaceClientCapabilities.php │ │ │ ├── WorkspaceEditClientCapabilities.php │ │ │ ├── WorkspaceFoldersServerCapabilities.php │ │ │ └── WorkspaceServerCapabilities.php │ │ ├── CodeAction │ │ │ ├── CodeActionContext.php │ │ │ ├── CodeActionFeature.php │ │ │ └── CodeActionProvider.php │ │ ├── Command │ │ │ ├── CommandFeature.php │ │ │ └── CommandProvider.php │ │ ├── Common │ │ │ ├── Command.php │ │ │ ├── Location.php │ │ │ ├── LocationLink.php │ │ │ ├── MarkupContent.php │ │ │ ├── MarkupKind.php │ │ │ ├── Position.php │ │ │ ├── Range.php │ │ │ ├── SymbolInformation.php │ │ │ ├── SymbolKind.php │ │ │ ├── TextDocumentEdit.php │ │ │ ├── TextDocumentIdentifier.php │ │ │ ├── TextDocumentItem.php │ │ │ ├── TextEdit.php │ │ │ ├── VersionedTextDocumentIdentifier.php │ │ │ └── WorkspaceEdit.php │ │ ├── Completion │ │ │ ├── CompletionContext.php │ │ │ ├── CompletionFeature.php │ │ │ ├── CompletionItem.php │ │ │ ├── CompletionItemKind.php │ │ │ ├── CompletionList.php │ │ │ ├── CompletionProvider.php │ │ │ ├── CompletionTriggerKind.php │ │ │ └── InsertTextFormat.php │ │ ├── Configuration │ │ │ ├── ConfigurationFeature.php │ │ │ └── ConfigurationItem.php │ │ ├── Diagnostics │ │ │ ├── Diagnostic.php │ │ │ ├── DiagnosticSeverity.php │ │ │ ├── DiagnosticsFeature.php │ │ │ ├── DiagnosticsProvider.php │ │ │ └── WorkspaceDiagnosticsProvider.php │ │ ├── DocumentSymbols │ │ │ ├── DocumentSymbol.php │ │ │ ├── DocumentSymbolsFeature.php │ │ │ └── DocumentSymbolsProvider.php │ │ ├── Feature.php │ │ ├── FileWatcher │ │ │ ├── DidChangeWatchedFilesRegistrationOptions.php │ │ │ ├── FileChangeType.php │ │ │ ├── FileEvent.php │ │ │ ├── FileSystemWatcher.php │ │ │ ├── FileWatcherFeature.php │ │ │ └── WatchKind.php │ │ ├── GoToDefinition │ │ │ ├── GoToDefinitionFeature.php │ │ │ └── GoToDefinitionProvider.php │ │ ├── GoToImplementation │ │ │ ├── GoToImplementationFeature.php │ │ │ └── GoToImplementationProvider.php │ │ ├── Hover │ │ │ ├── Hover.php │ │ │ ├── HoverFeature.php │ │ │ ├── HoverProvider.php │ │ │ └── MarkedString.php │ │ ├── LanguageServer.php │ │ ├── Message │ │ │ ├── MessageActionItem.php │ │ │ ├── MessageFeature.php │ │ │ └── MessageType.php │ │ ├── ProgressNotification │ │ │ ├── Progress.php │ │ │ ├── ProgressGroup.php │ │ │ └── ProgressNotificationFeature.php │ │ ├── References │ │ │ ├── ReferenceContext.php │ │ │ ├── ReferencesFeature.php │ │ │ └── ReferencesProvider.php │ │ ├── Registration │ │ │ ├── Registration.php │ │ │ ├── RegistrationFeature.php │ │ │ └── Unregistration.php │ │ ├── SignatureHelp │ │ │ ├── ParameterInformation.php │ │ │ ├── SignatureHelp.php │ │ │ ├── SignatureHelpFeature.php │ │ │ ├── SignatureHelpProvider.php │ │ │ └── SignatureInformation.php │ │ ├── TextDocument │ │ │ ├── TextDocumentContentChangeEvent.php │ │ │ └── TextDocumentFeature.php │ │ ├── Workspace │ │ │ ├── WorkspaceFeature.php │ │ │ ├── WorkspaceFolder.php │ │ │ └── WorkspaceFoldersChangeEvent.php │ │ ├── WorkspaceEdit │ │ │ ├── ApplyWorkspaceEditResponse.php │ │ │ └── WorkspaceEditFeature.php │ │ └── WorkspaceSymbols │ │ │ ├── WorkspaceSymbolsFeature.php │ │ │ └── WorkspaceSymbolsProvider.php │ ├── Index │ │ ├── FileFilterFactory.php │ │ ├── FileWatcherHandler.php │ │ ├── GlobalIndexer.php │ │ ├── Index.php │ │ ├── IndexDataProvider.php │ │ ├── IndexEntry.php │ │ ├── IndexStorageFactory.php │ │ ├── Indexer.php │ │ ├── LocalCacheIndexStorageFactory.php │ │ ├── MemoryIndexStorageFactory.php │ │ ├── Query.php │ │ └── Storage │ │ │ ├── ChainedStorage.php │ │ │ ├── IndexStorage.php │ │ │ ├── MemoryStorage.php │ │ │ ├── MergedStorage.php │ │ │ ├── OpenDocumentsStorage.php │ │ │ ├── SqliteStorage.php │ │ │ └── WritableIndexStorage.php │ ├── Io │ │ ├── Directories.php │ │ ├── FileLister │ │ │ ├── FileFilter.php │ │ │ ├── FileLister.php │ │ │ ├── GlobFileFilter.php │ │ │ ├── GlobRejectDirectoryFilter.php │ │ │ └── LocalFileLister.php │ │ ├── FileReader.php │ │ ├── FileWatcher │ │ │ ├── ClosedDocumentFileWatcher.php │ │ │ ├── FileChangeDeduplicator.php │ │ │ ├── FileWatcher.php │ │ │ └── InotifyWaitFileWatcher.php │ │ └── LocalFileReader.php │ ├── Logger │ │ ├── ClientLogger.php │ │ ├── CompositeLogger.php │ │ ├── LevelFilteringLogger.php │ │ ├── LoggerTrait.php │ │ └── StreamLogger.php │ ├── Mapper │ │ ├── PrefixStrippingUriMapper.php │ │ └── UriMapper.php │ ├── Plugin.php │ ├── PluginFinder.php │ ├── ServerPlugin.php │ ├── ServerPluginInit.php │ ├── Tenkawa.php │ ├── Transport │ │ ├── RunnableTransport.php │ │ └── StreamTransport.php │ ├── Uri.php │ └── Utils │ │ ├── Cache.php │ │ ├── Event.php │ │ ├── FuzzyMatcher.php │ │ ├── InfiniteRecursionMarker.php │ │ ├── KeyValueStateTrait.php │ │ ├── NestedKernelsSyncAsync.php │ │ ├── Platform.php │ │ ├── PositionUtils.php │ │ ├── PriorityKernel │ │ ├── Priority.php │ │ ├── PriorityStrand.php │ │ ├── ReactScheduler.php │ │ ├── ScheduledApi.php │ │ ├── ScheduledReactKernel.php │ │ └── Scheduler.php │ │ ├── Stopwatch.php │ │ ├── StringTemplate.php │ │ ├── StringUtils.php │ │ ├── SyncAsync.php │ │ ├── SyncCallContext.php │ │ ├── Template.php │ │ └── Throttler.php │ ├── Symfony │ ├── Container │ │ ├── ServiceMapUpdater.php │ │ └── ServiceMapWatcher.php │ └── SymfonyPlugin.php │ └── WebMozartAssert │ └── WebMozartAssertPlugin.php └── tests └── Tsufeki └── Tenkawa ├── DummyTransportPair.php ├── FunctionalTest.php ├── Php ├── Parser │ └── FindNodeVisitorTest.php └── Reflection │ └── ReflectionVisitorTest.php ├── Server ├── Document │ └── DocumentStoreTest.php ├── Event │ └── EventDispatcherTest.php ├── Feature │ └── Diagnostics │ │ └── DiagnosticsFeatureTest.php ├── Index │ └── Storage │ │ ├── ChainedStorageTest.php │ │ ├── MemoryStorageTest.php │ │ ├── MergedStorageTest.php │ │ ├── SqliteStorageTest.php │ │ ├── SqliteStorageWithPrefixTest.php │ │ └── WritableIndexStorageTest.php ├── Transport │ └── StreamTransportTest.php ├── UriTest.php └── Utils │ ├── EventTest.php │ ├── PositionUtilsTest.php │ ├── StringUtilsTest.php │ └── SyncAsyncTest.php ├── TestCase.php └── fixtures └── Foo └── SelfCompletion.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /data/ 3 | /.php_cs.cache 4 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(__DIR__); 5 | 6 | return PhpCsFixer\Config::create() 7 | ->setRules([ 8 | '@Symfony' => true, 9 | 'align_multiline_comment' => [ 10 | 'comment_type' => 'all_multiline', 11 | ], 12 | 'array_syntax' => [ 13 | 'syntax' => 'short', 14 | ], 15 | 'blank_line_after_opening_tag' => false, 16 | 'blank_line_before_statement' => [ 17 | 'statements' => ['return', 'throw', 'try'], 18 | ], 19 | 'braces' => [ 20 | 'allow_single_line_closure' => true, 21 | ], 22 | 'cast_spaces' => [ 23 | 'space' => 'none', 24 | ], 25 | 'concat_space' => [ 26 | 'spacing' => 'one', 27 | ], 28 | 'declare_strict_types' => true, 29 | 'increment_style' => [ 30 | 'style' => 'post', 31 | ], 32 | 'is_null' => [ 33 | 'use_yoda_style' => false, 34 | ], 35 | 'modernize_types_casting' => true, 36 | 'no_alias_functions' => true, 37 | 'no_break_comment' => false, 38 | 'no_superfluous_elseif' => true, 39 | 'no_useless_else' => true, 40 | 'ordered_imports' => true, 41 | 'php_unit_construct' => true, 42 | 'php_unit_dedicate_assert' => true, 43 | 'php_unit_expectation' => true, 44 | 'php_unit_mock' => true, 45 | 'php_unit_namespaced' => true, 46 | 'php_unit_no_expectation_annotation' => true, 47 | 'php_unit_test_class_requires_covers' => true, 48 | 'phpdoc_annotation_without_dot' => false, 49 | 'phpdoc_summary' => false, 50 | 'phpdoc_to_comment' => false, 51 | 'pow_to_exponentiation' => true, 52 | 'random_api_migration' => true, 53 | 'space_after_semicolon' => [ 54 | 'remove_in_empty_for_expressions' => true, 55 | ], 56 | 'ternary_to_null_coalescing' => true, 57 | 'yoda_style' => false, 58 | ]) 59 | ->setFinder($finder); 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.1' 5 | - '7.2' 6 | - '7.3' 7 | - '7.4' 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache 12 | 13 | before_install: 14 | - phpenv config-rm xdebug.ini 15 | 16 | install: 17 | - composer install --no-interaction 18 | 19 | script: 20 | - ./vendor/bin/phpstan analyze --configuration=phpstan.neon --level=max --no-interaction --no-progress src/ tests/ bin/ 21 | - ./vendor/bin/phpunit 22 | -------------------------------------------------------------------------------- /bin/tenkawa.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 7.1\n"); 9 | exit(1); 10 | } 11 | 12 | foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) { 13 | if (file_exists($file)) { 14 | require_once $file; 15 | break; 16 | } 17 | } 18 | unset($file); 19 | 20 | $xdebug = new XdebugHandler('tenkawa'); 21 | $xdebug->check(); 22 | unset($xdebug); 23 | 24 | $requiredExtensions = [ 25 | 'pdo_sqlite' => 2, 26 | 'mbstring' => 3, 27 | ]; 28 | 29 | foreach ($requiredExtensions as $ext => $errorCode) { 30 | if (!extension_loaded($ext)) { 31 | fprintf(STDERR, "Tenkawa requires $ext extension\n"); 32 | exit($errorCode); 33 | } 34 | } 35 | unset($requiredExtensions, $ext, $errorCode); 36 | 37 | if (!class_exists(Tenkawa::class)) { 38 | fprintf(STDERR, "Tenkawa was not properly installed\n"); 39 | exit(9); 40 | } 41 | 42 | Tenkawa::main($argv); 43 | -------------------------------------------------------------------------------- /images/tenkawa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsufeki/tenkawa-php-language-server/90ffe347a664a39af538ea943147081e9d341b45/images/tenkawa-logo.png -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-phpunit/extension.neon 3 | - vendor/phpstan/phpstan-phpunit/rules.neon 4 | 5 | parameters: 6 | excludes_analyse: 7 | - '%rootDir%/../../../tests/Tsufeki/Tenkawa/fixtures/*' 8 | ignoreErrors: 9 | - '#Call to an undefined static method Recoil\\Recoil::eventLoop\(\)#' 10 | - '#(Tsufeki\\Tenkawa\\Php\\PhpStan\\.*|Anonymous.*)::__construct\(\) does not call parent constructor from (PHPStan\\.*|Reflection(Class|Property|Method)|PhpParser\\PrettyPrinterAbstract|Tsufeki\\Tenkawa\\Php\\PhpStan\\Utils\\DocumentParser)#' 11 | - '#Call to an undefined method PDO::sqliteCreateFunction\(\)#' 12 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/BeberleiAssert/BeberleiAssertPlugin.php: -------------------------------------------------------------------------------- 1 | setClass(DynamicFunctionReturnTypeExtension::class, AssertThatFunctionDynamicReturnTypeExtension::class, true); 23 | $container->setClass(DynamicMethodReturnTypeExtension::class, AssertionChainDynamicReturnTypeExtension::class, true); 24 | $container->setClass(DynamicStaticMethodReturnTypeExtension::class, AssertThatDynamicMethodReturnTypeExtension::class, true); 25 | $container->setClass(MethodTypeSpecifyingExtension::class, AssertionChainTypeSpecifyingExtension::class, true); 26 | $container->setClass(StaticMethodTypeSpecifyingExtension::class, AssertTypeSpecifyingExtension::class, true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Doctrine/DoctrinePlugin.php: -------------------------------------------------------------------------------- 1 | setClass(DynamicMethodReturnTypeExtension::class, DoctrineSelectableDynamicReturnTypeExtension::class, true); 22 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityManagerFindDynamicReturnTypeExtension::class, true); 23 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityManagerGetRepositoryDynamicReturnTypeExtension::class, true, [new Value('Doctrine\ORM\EntityRepository')]); 24 | $container->setClass(DynamicMethodReturnTypeExtension::class, EntityRepositoryDynamicReturnTypeExtension::class, true); 25 | $container->setClass(DynamicMethodReturnTypeExtension::class, ObjectManagerMergeDynamicReturnTypeExtension::class, true); 26 | $container->setClass(MethodsClassReflectionExtension::class, DoctrineSelectableClassReflectionExtension::class, true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Phony/PhonyPlugin.php: -------------------------------------------------------------------------------- 1 | setClass(DynamicMethodReturnTypeExtension::class, InstanceHandleGetReturnType::class, true); 23 | $container->setClass(DynamicMethodReturnTypeExtension::class, MockBuilderGetReturnType::class, true); 24 | $container->setClass(PropertiesClassReflectionExtension::class, HandleProperties::class, true); 25 | 26 | foreach ([ 27 | 'Eloquent\\Phony', 28 | 'Eloquent\\Phony\\Kahlan', 29 | 'Eloquent\\Phony\\Phpunit', 30 | 'Eloquent\\Phony\\Pho', 31 | ] as $namespace) { 32 | $container->setClass(MockBuilderReturnType::class, null, false, [new Value($namespace)]); 33 | $container->setAlias(DynamicFunctionReturnTypeExtension::class, MockBuilderReturnType::class, true); 34 | $container->setAlias(DynamicStaticMethodReturnTypeExtension::class, MockBuilderReturnType::class, true); 35 | 36 | $container->setClass(MockReturnType::class, null, false, [new Value($namespace)]); 37 | $container->setAlias(DynamicFunctionReturnTypeExtension::class, MockReturnType::class, true); 38 | $container->setAlias(DynamicStaticMethodReturnTypeExtension::class, MockReturnType::class, true); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Composer/ComposerFileFilter.php: -------------------------------------------------------------------------------- 1 | rejectGlobs = $this->normalize($rejectGlobs); 34 | $this->acceptGlobs = $this->normalize($acceptGlobs); 35 | $this->forceRejectGlobs = $this->normalize($forceRejectGlobs); 36 | } 37 | 38 | private function normalize(array $globs): array 39 | { 40 | return array_map(function ($glob) { 41 | return Uri::fromString($glob)->getNormalizedGlob(); 42 | }, array_unique($globs)); 43 | } 44 | 45 | public function filter(string $uri, string $baseUri): int 46 | { 47 | $accept = !$this->matchArray($this->rejectGlobs, $uri) 48 | || ($this->matchArray($this->acceptGlobs, $uri) 49 | && !$this->matchArray($this->forceRejectGlobs, $uri)); 50 | 51 | return $accept ? self::ABSTAIN : self::REJECT; 52 | } 53 | 54 | public function getFileType(): string 55 | { 56 | return ''; 57 | } 58 | 59 | public function enterDirectory(string $uri, string $baseUri): int 60 | { 61 | return self::ABSTAIN; 62 | } 63 | 64 | private function matchArray(array $globs, string $uri): bool 65 | { 66 | foreach ($globs as $glob) { 67 | if (Glob::match($uri, $glob)) { 68 | return true; 69 | } 70 | } 71 | 72 | return false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/Completion/SymbolCompleter.php: -------------------------------------------------------------------------------- 1 | symbolExtractor = $symbolExtractor; 33 | $this->symbolCompleters = $symbolCompleters; 34 | } 35 | 36 | public function getTriggerCharacters(): array 37 | { 38 | return array_merge(...array_map(function (SymbolCompleter $completer) { 39 | return $completer->getTriggerCharacters(); 40 | }, $this->symbolCompleters)); 41 | } 42 | 43 | public function getCompletions( 44 | Document $document, 45 | Position $position, 46 | ?CompletionContext $context 47 | ): \Generator { 48 | if ($document->getLanguage() !== 'php') { 49 | return new CompletionList(); 50 | } 51 | 52 | /** @var Symbol|null */ 53 | $symbol = yield $this->symbolExtractor->getSymbolAt($document, $position, true); 54 | if ($symbol === null) { 55 | return new CompletionList(); 56 | } 57 | 58 | return CompletionList::merge(yield array_map(function (SymbolCompleter $completer) use ($symbol, $position) { 59 | return $completer->getCompletions($symbol, $position); 60 | }, $this->symbolCompleters)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/Completion/VariableGatheringVisitor.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private $variables; 19 | 20 | /** 21 | * @param array $initialVariables 22 | */ 23 | public function __construct(array $initialVariables = []) 24 | { 25 | $this->variables = $initialVariables; 26 | } 27 | 28 | public function enterNode(Node $node) 29 | { 30 | if ($node instanceof Expr\Variable && is_string($node->name)) { 31 | $this->variables[$node->name] = $node->getAttribute('type'); 32 | 33 | return null; 34 | } 35 | 36 | if ($node instanceof FunctionLike || $node instanceof Stmt\ClassLike) { 37 | return NodeTraverser::DONT_TRAVERSE_CHILDREN; 38 | } 39 | 40 | return null; 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function getVariables(): array 47 | { 48 | return $this->variables; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/GoToImplementation/FindClassLikeImplementationsVisitor.php: -------------------------------------------------------------------------------- 1 | root = $root; 24 | } 25 | 26 | public function enter(ResolvedClassLike $class): \Generator 27 | { 28 | if ($class->location != $this->root->location) { 29 | $this->implementations[] = $class; 30 | } 31 | 32 | return; 33 | yield; 34 | } 35 | 36 | public function leave(ResolvedClassLike $class): \Generator 37 | { 38 | return; 39 | yield; 40 | } 41 | 42 | /** 43 | * @return ResolvedClassLike[] 44 | */ 45 | public function getImplementations(): array 46 | { 47 | // deduplicate 48 | $result = []; 49 | foreach ($this->implementations as $impl) { 50 | foreach ($result as $prevImpl) { 51 | if ($impl->location == $prevImpl->location) { 52 | continue 2; 53 | } 54 | } 55 | $result[] = $impl; 56 | } 57 | 58 | return $result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/Hover/ExpressionTypeHoverProvider.php: -------------------------------------------------------------------------------- 1 | typeInference = $typeInference; 35 | $this->nodeFinder = $nodeFinder; 36 | $this->formatter = $formatter; 37 | } 38 | 39 | public function getHover(Document $document, Position $position): \Generator 40 | { 41 | if ($document->getLanguage() !== 'php') { 42 | return null; 43 | } 44 | 45 | /** @var (Node|Comment)[] $nodes */ 46 | $nodes = yield $this->nodeFinder->getNodePath($document, $position); 47 | yield $this->typeInference->infer($document, $nodes); 48 | 49 | if (!empty($nodes) && $nodes[0] instanceof Node\Name) { 50 | array_shift($nodes); 51 | } 52 | 53 | if (!empty($nodes) && $nodes[0] instanceof Node) { 54 | $type = $nodes[0]->getAttribute('type', null); 55 | if ($type !== null) { 56 | $hover = new Hover(); 57 | $hover->contents = $this->formatter->formatExpression($type); 58 | $hover->range = PositionUtils::rangeFromNodeAttrs($nodes[0]->getAttributes(), $document); 59 | 60 | return $hover; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/Refactoring/Differ.php: -------------------------------------------------------------------------------- 1 | tabs = $tabs; 20 | $this->spaces = $spaces; 21 | } 22 | 23 | public function render(): string 24 | { 25 | return str_repeat("\t", $this->tabs) . str_repeat(' ', $this->spaces); 26 | } 27 | 28 | public static function min(self $a, self $b): self 29 | { 30 | if ($a->tabs === $b->tabs) { 31 | return $a->spaces < $b->spaces ? $a : $b; 32 | } 33 | 34 | return $a->tabs < $b->tabs ? $a : $b; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/Refactoring/WorkspaceEditCommandProvider.php: -------------------------------------------------------------------------------- 1 | workspaceEditFeature = $workspaceEditFeature; 21 | } 22 | 23 | public function getCommand(): string 24 | { 25 | return self::COMMAND; 26 | } 27 | 28 | public function execute(string $label, WorkspaceEdit $edit): \Generator 29 | { 30 | yield $this->workspaceEditFeature->applyWorkspaceEdit($label, $edit); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Feature/References/Reference.php: -------------------------------------------------------------------------------- 1 | nodesToTag = $nodesToTag; 23 | } 24 | 25 | public function enterNode(Node $node) 26 | { 27 | parent::enterNode($node); 28 | 29 | if ($this->nodesToTag->contains($node)) { 30 | $node->setAttribute('nameContext', clone $this->nameContext); 31 | } 32 | 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Parser/Ast.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 22 | } 23 | 24 | public function getDiagnostics(Document $document): \Generator 25 | { 26 | if ($document->getLanguage() !== 'php') { 27 | return []; 28 | } 29 | 30 | /** @var Ast $ast */ 31 | $ast = yield $this->parser->parse($document); 32 | 33 | return array_map(function (Error $error) use ($document) { 34 | $diag = new Diagnostic(); 35 | $diag->range = PositionUtils::rangeFromNodeAttrs($error->getAttributes(), $document); 36 | $diag->severity = DiagnosticSeverity::ERROR; 37 | $diag->source = 'php'; 38 | $diag->message = $error->getRawMessage(); 39 | 40 | return $diag; 41 | }, $ast->errors); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/Analyser/AnalysedCacheAware.php: -------------------------------------------------------------------------------- 1 | methodReflection = $methodReflection; 15 | } 16 | 17 | public function isGenerator() 18 | { 19 | return $this->methodReflection->isGenerator(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/IndexReflection/DummyReflectionProperty.php: -------------------------------------------------------------------------------- 1 | propertyReflection = $propertyReflection; 22 | $this->propertyName = $name; 23 | } 24 | 25 | public function getName() 26 | { 27 | return $this->propertyName; 28 | } 29 | 30 | public function getDeclaringClass() 31 | { 32 | $declaringClass = $this->propertyReflection->getDeclaringClass(); 33 | assert($declaringClass instanceof IndexClassReflection); 34 | 35 | return new DummyReflectionClass($declaringClass); 36 | } 37 | 38 | public function isPrivate() 39 | { 40 | return $this->propertyReflection->isPrivate(); 41 | } 42 | 43 | public function isProtected() 44 | { 45 | return !$this->propertyReflection->isPublic() && !$this->propertyReflection->isPrivate(); 46 | } 47 | 48 | public function isStatic() 49 | { 50 | return $this->propertyReflection->isStatic(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/IndexReflection/DummyReflectionType.php: -------------------------------------------------------------------------------- 1 | string = ltrim($string, '\\'); 30 | $this->allowsNull = $allowsNull; 31 | $this->isBuiltin = $isBuiltin; 32 | } 33 | 34 | public function allowsNull() 35 | { 36 | return $this->allowsNull; 37 | } 38 | 39 | public function isBuiltin() 40 | { 41 | return $this->isBuiltin; 42 | } 43 | 44 | public function __toString() 45 | { 46 | return $this->string; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/PhpDocResolver/PhpDocResolverVisitor.php: -------------------------------------------------------------------------------- 1 | comment => name context 13 | */ 14 | private $nameContexts = []; 15 | 16 | /** 17 | * @var NameContext|null 18 | */ 19 | private $lastNameContext; 20 | 21 | public function enterNode(Node $node) 22 | { 23 | parent::enterNode($node); 24 | 25 | $phpDoc = $node->getDocComment(); 26 | if ($phpDoc !== null) { 27 | if ($this->lastNameContext === null || $this->lastNameContext != $this->nameContext) { 28 | $this->lastNameContext = clone $this->nameContext; 29 | } 30 | $this->nameContexts[$phpDoc->getText()] = $this->lastNameContext; 31 | } 32 | } 33 | 34 | /** 35 | * @return array comment => name context 36 | */ 37 | public function getNameContexts(): array 38 | { 39 | return $this->nameContexts; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/Utils/DocumentParser.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 34 | $this->syncAsync = $syncAsync; 35 | } 36 | 37 | public function setDocument(?Document $document): void 38 | { 39 | $this->document = $document; 40 | } 41 | 42 | public function parseFile(string $file): array 43 | { 44 | if ($this->document === null) { 45 | throw new ShouldNotHappenException(); 46 | } 47 | 48 | if (!$this->document->getUri()->equals(Uri::fromFilesystemPath($file))) { 49 | throw new ShouldNotHappenException(); 50 | } 51 | 52 | /** @var Ast $ast */ 53 | $ast = $this->syncAsync->callAsync($this->parser->parse($this->document)); 54 | 55 | return $ast->nodes; 56 | } 57 | 58 | public function parseString(string $sourceCode): array 59 | { 60 | throw new ShouldNotHappenException(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/PhpStan/Utils/ErrorTolerantPrettyPrinter.php: -------------------------------------------------------------------------------- 1 | getConstantValue($name); 14 | } 15 | 16 | return \constant($name); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Reflection/ClassResolverExtension.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 31 | $this->reflectionProvider = $reflectionProvider; 32 | $this->classResolver = $classResolver; 33 | } 34 | 35 | /** 36 | * @resolve mixed 37 | */ 38 | public function evaluate(string $expr, NameContext $nameContext, Document $document): \Generator 39 | { 40 | return yield (new ConstExprEvaluation( 41 | $this->parser, 42 | $this->reflectionProvider, 43 | $this->classResolver, 44 | $document 45 | ))->evaluate($expr, $nameContext); 46 | } 47 | 48 | /** 49 | * @resolve mixed 50 | */ 51 | public function getConstValue(Element\Const_ $const, Document $document): \Generator 52 | { 53 | return yield (new ConstExprEvaluation( 54 | $this->parser, 55 | $this->reflectionProvider, 56 | $this->classResolver, 57 | $document 58 | ))->getConstValue($const); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Reflection/Element/ClassConst.php: -------------------------------------------------------------------------------- 1 | location === null) { 45 | return null; 46 | } 47 | 48 | $link = new LocationLink(); 49 | $link->originSelectionRange = $originSelectionRange; 50 | $link->targetUri = $this->location->uri; 51 | $link->targetRange = $this->location->range; 52 | $link->targetSelectionRange = $this->nameRange ?? $this->location->range; 53 | 54 | return $link; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Reflection/Element/Function_.php: -------------------------------------------------------------------------------- 1 | classResolver = $classResolver; 23 | $this->reflectionProvider = $reflectionProvider; 24 | } 25 | 26 | /** 27 | * @param InheritanceTreeVisitor[] $visitors 28 | */ 29 | public function traverse(string $className, array $visitors, Document $document): \Generator 30 | { 31 | yield $this->traverseClass($className, $visitors, $document, new Cache()); 32 | } 33 | 34 | /** 35 | * @param InheritanceTreeVisitor[] $visitors 36 | */ 37 | private function traverseClass(string $className, array $visitors, Document $document, Cache $cache): \Generator 38 | { 39 | $class = yield $this->classResolver->resolve($className, $document, $cache); 40 | if ($class === null) { 41 | return; 42 | } 43 | 44 | foreach ($visitors as $visitor) { 45 | yield $visitor->enter($class); 46 | } 47 | 48 | foreach (yield $this->reflectionProvider->getInheritingClasses($document, $className) as $inheritingClassName) { 49 | yield $this->traverseClass($inheritingClassName, $visitors, $document, $cache); 50 | } 51 | 52 | foreach ($visitors as $visitor) { 53 | yield $visitor->leave($class); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Reflection/InheritanceTreeVisitor.php: -------------------------------------------------------------------------------- 1 | withLineNumber($node->getLine()); 21 | 22 | return self::ANONYMOUS_CLASS_PREFIX . sha1($uri->getNormalized()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/Reflection/ReflectionTransformer.php: -------------------------------------------------------------------------------- 1 | description; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/TypeInference/IntersectionType.php: -------------------------------------------------------------------------------- 1 | types); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/TypeInference/ObjectType.php: -------------------------------------------------------------------------------- 1 | class = $class; 12 | } 13 | 14 | public function __toString(): string 15 | { 16 | return $this->class; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Php/TypeInference/Type.php: -------------------------------------------------------------------------------- 1 | types); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/PhpUnit/MockBuilderDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | setClass(DynamicMethodReturnTypeExtension::class, CreateMockDynamicReturnTypeExtension::class, true); 26 | $container->setClass(DynamicMethodReturnTypeExtension::class, GetMockBuilderDynamicReturnTypeExtension::class, true); 27 | $container->setClass(DynamicMethodReturnTypeExtension::class, MockBuilderDynamicReturnTypeExtension::class, true); 28 | $container->setClass(FunctionTypeSpecifyingExtension::class, AssertFunctionTypeSpecifyingExtension::class, true); 29 | $container->setClass(MethodTypeSpecifyingExtension::class, AssertMethodTypeSpecifyingExtension::class, true); 30 | $container->setClass(StaticMethodTypeSpecifyingExtension::class, AssertStaticMethodTypeSpecifyingExtension::class, true); 31 | 32 | $container->setClass(Rule::class, AssertSameBooleanExpectedRule::class, true); 33 | $container->setClass(Rule::class, AssertSameNullExpectedRule::class, true); 34 | $container->setClass(Rule::class, AssertSameWithCountRule::class, true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Prophecy/ProphecyPlugin.php: -------------------------------------------------------------------------------- 1 | setClass(DynamicMethodReturnTypeExtension::class, ObjectProphecyRevealDynamicReturnTypeExtension::class, true); 19 | $container->setClass(DynamicMethodReturnTypeExtension::class, TestCaseProphesizeDynamicReturnTypeExtension::class, true); 20 | $container->setClass(DynamicMethodReturnTypeExtension::class, ProphetProphesizeDynamicReturnTypeExtension::class, true); 21 | $container->setClass(MethodsClassReflectionExtension::class, ProphecyMethodsClassReflectionExtension::class, true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Document/Document.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 35 | $this->language = $language; 36 | } 37 | 38 | public function getUri(): Uri 39 | { 40 | return $this->uri; 41 | } 42 | 43 | public function getVersion(): ?int 44 | { 45 | return $this->version; 46 | } 47 | 48 | public function getLanguage(): string 49 | { 50 | return $this->language; 51 | } 52 | 53 | public function getText(): string 54 | { 55 | return $this->text; 56 | } 57 | 58 | public function update(string $text, ?int $version = null): self 59 | { 60 | $this->text = $text; 61 | $this->version = $version; 62 | $this->data = []; 63 | 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Document/Project.php: -------------------------------------------------------------------------------- 1 | rootUri = $rootUri; 20 | } 21 | 22 | public function getRootUri(): Uri 23 | { 24 | return $this->rootUri; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Event/Document/OnChange.php: -------------------------------------------------------------------------------- 1 | container = $container; 23 | $this->timeout = 120.0; 24 | } 25 | 26 | /** 27 | * Dispatch coroutines in a new strand and return immediately. 28 | */ 29 | public function dispatch(string $event, ...$args): \Generator 30 | { 31 | yield Recoil::execute(Recoil::timeout($this->timeout, $this->dispatchAndWait($event, ...$args))); 32 | } 33 | 34 | /** 35 | * Dispatch and wait until all dispatched coroutines finish. 36 | */ 37 | public function dispatchAndWait(string $event, ...$args): \Generator 38 | { 39 | $parts = explode('\\', $event); 40 | $method = end($parts); 41 | $listeners = $this->container->getOrDefault($event, []); 42 | 43 | yield array_map(function ($listener) use ($method, $args) { 44 | return $listener->$method(...$args); 45 | }, $listeners); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Event/OnFileChange.php: -------------------------------------------------------------------------------- 1 | uri = $this->targetUri; 47 | $location->range = $this->targetRange; 48 | 49 | return $location; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/Common/MarkupContent.php: -------------------------------------------------------------------------------- 1 | line = $line; 43 | $this->character = $character; 44 | } 45 | 46 | public function __toString(): string 47 | { 48 | return "($this->line,$this->character)"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/Common/Range.php: -------------------------------------------------------------------------------- 1 | start = $start; 33 | $this->end = $end; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/Common/SymbolInformation.php: -------------------------------------------------------------------------------- 1 | |null URI => TextEdit[]. 18 | */ 19 | public $changes; 20 | 21 | /** 22 | * An array of `TextDocumentEdit`s to express changes to n different text documents 23 | * 24 | * ...where each text document edit addresses a specific version of a text 25 | * document. Whether a client supports versioned document edits is 26 | * expressed via `WorkspaceClientCapabilities.workspaceEdit.documentChanges`. 27 | * 28 | * @var TextDocumentEdit[]|null 29 | */ 30 | public $documentChanges; 31 | } 32 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/Completion/CompletionContext.php: -------------------------------------------------------------------------------- 1 | items = array_merge(...array_map(function (CompletionList $list) { 37 | return array_values($list->items); 38 | }, $completionsLists)); 39 | 40 | $completions->isIncomplete = 0 !== array_sum(array_map(function (CompletionList $list) { 41 | return $list->isIncomplete; 42 | }, $completionsLists)); 43 | 44 | return $completions; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/Completion/CompletionProvider.php: -------------------------------------------------------------------------------- 1 | URI => diagnostics 13 | */ 14 | public function getWorkspaceDiagnostics(array $documents): \Generator; 15 | } 16 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/DocumentSymbols/DocumentSymbol.php: -------------------------------------------------------------------------------- 1 | progressGroup = $progressGroup; 20 | } 21 | 22 | public function __destruct() 23 | { 24 | $this->done(); 25 | } 26 | 27 | public function set(string $label, ?int $status = null): void 28 | { 29 | $this->progressGroup->progress($label, $status); 30 | } 31 | 32 | public function done(): void 33 | { 34 | if (!$this->done) { 35 | $this->done = true; 36 | $this->progressGroup->done(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/ProgressNotification/ProgressGroup.php: -------------------------------------------------------------------------------- 1 | id = $id; 25 | $this->progressCallback = $progressCallback; 26 | } 27 | 28 | public function get(): Progress 29 | { 30 | $this->activeCount++; 31 | 32 | return new Progress($this); 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | public function progress(string $label, ?int $status = null): void 39 | { 40 | ($this->progressCallback)($this->id, $label, $status); 41 | } 42 | 43 | /** 44 | * @internal 45 | */ 46 | public function done(): void 47 | { 48 | $this->activeCount--; 49 | if ($this->activeCount <= 0) { 50 | ($this->progressCallback)($this->id, null, null, true); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/ProgressNotification/ProgressNotificationFeature.php: -------------------------------------------------------------------------------- 1 | rpc = $rpc; 29 | $this->kernel = $kernel; 30 | } 31 | 32 | public function initialize(ClientCapabilities $clientCapabilities, ServerCapabilities $serverCapabilities): \Generator 33 | { 34 | return; 35 | yield; 36 | } 37 | 38 | public function create(): ProgressGroup 39 | { 40 | $callback = function (string $id, ?string $label = null, ?int $status = null, bool $done = false) { 41 | $this->kernel->execute($this->progress($id, $label, $status, $done)); 42 | }; 43 | 44 | return new ProgressGroup($this->generateId(), $callback); 45 | } 46 | 47 | private function progress(string $id, ?string $label = null, ?int $status = null, bool $done = false): \Generator 48 | { 49 | if ($this->rpc !== null) { 50 | yield $this->rpc->notify('$/tenkawaphp/window/progress', compact('id', 'label', 'status', 'done')); 51 | } 52 | } 53 | 54 | private function generateId(): string 55 | { 56 | return uniqid('', true); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/References/ReferenceContext.php: -------------------------------------------------------------------------------- 1 | rpc = $rpc; 29 | $this->logger = $logger; 30 | } 31 | 32 | public function initialize(ClientCapabilities $clientCapabilities, ServerCapabilities $serverCapabilities): \Generator 33 | { 34 | return; 35 | yield; 36 | } 37 | 38 | /** 39 | * The workspace/applyEdit request is sent from the server to the client to 40 | * modify resource on the client side. 41 | * 42 | * @param string|null $label An optional label of the workspace edit. 43 | * This label is presented in the user interface 44 | * for example on an undo stack to undo the workspace edit. 45 | * @param WorkspaceEdit $edit The edits to apply. 46 | * 47 | * @resolve ApplyWorkspaceEditResponse 48 | */ 49 | public function applyWorkspaceEdit(?string $label, WorkspaceEdit $edit): \Generator 50 | { 51 | $this->logger->debug('send: ' . __FUNCTION__); 52 | 53 | return yield $this->rpc->call('workspace/applyEdit', compact('label', 'edit'), ApplyWorkspaceEditResponse::class); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Feature/WorkspaceSymbols/WorkspaceSymbolsProvider.php: -------------------------------------------------------------------------------- 1 | documentStore = $documentStore; 20 | } 21 | 22 | /** 23 | * @param Document|Project $documentOrProject 24 | * 25 | * @resolve IndexEntry[] 26 | */ 27 | public function search($documentOrProject, Query $query, bool $projectOnly = false): \Generator 28 | { 29 | if ($documentOrProject instanceof Document) { 30 | /** @var Project $project */ 31 | $project = yield $this->documentStore->getProjectForDocument($documentOrProject); 32 | } else { 33 | $project = $documentOrProject; 34 | } 35 | 36 | /** @var IndexStorage $indexStorage */ 37 | $indexStorage = $project->get('index' . ($projectOnly ? '.project_only' : '')); 38 | 39 | return yield $indexStorage->search($query); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/IndexDataProvider.php: -------------------------------------------------------------------------------- 1 | cacheDir = $dirs->getCacheDir() . '/index'; 28 | $this->documentStore = $documentStore; 29 | } 30 | 31 | public function createOpenedFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage 32 | { 33 | return new OpenDocumentsStorage($project, $this->documentStore); 34 | } 35 | 36 | public function createProjectFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage 37 | { 38 | $hash = sha1((string)$project->getRootUri()); 39 | 40 | return new SqliteStorage( 41 | $this->cacheDir . "/project-$hash.sqlite", 42 | $indexDataVersion, 43 | (string)$project->getRootUri() 44 | ); 45 | } 46 | 47 | public function createStubsIndex(Uri $uri, string $indexDataVersion): WritableIndexStorage 48 | { 49 | $hash = sha1((string)$uri); 50 | 51 | return new SqliteStorage( 52 | $this->cacheDir . "/stubs-$hash.sqlite", 53 | $indexDataVersion, 54 | (string)$uri 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/MemoryIndexStorageFactory.php: -------------------------------------------------------------------------------- 1 | documentStore = $documentStore; 22 | } 23 | 24 | public function createOpenedFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage 25 | { 26 | return new OpenDocumentsStorage($project, $this->documentStore); 27 | } 28 | 29 | public function createProjectFilesIndex(Project $project, string $indexDataVersion): WritableIndexStorage 30 | { 31 | return new MemoryStorage(); 32 | } 33 | 34 | public function createStubsIndex(Uri $uri, string $indexDataVersion): WritableIndexStorage 35 | { 36 | return new MemoryStorage(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/Query.php: -------------------------------------------------------------------------------- 1 | primaryStorage = $primaryStorage; 29 | $this->secondaryStorage = $secondaryStorage; 30 | } 31 | 32 | public function search(Query $query): \Generator 33 | { 34 | $result = yield $this->primaryStorage->search($query); 35 | $primaryFiles = yield $this->primaryStorage->getFileStamps(); 36 | 37 | /** @var IndexEntry $entry */ 38 | foreach (yield $this->secondaryStorage->search($query) as $entry) { 39 | if (!array_key_exists($entry->sourceUri->getNormalized(), $primaryFiles)) { 40 | $result[] = $entry; 41 | } 42 | } 43 | 44 | return $result; 45 | } 46 | 47 | public function getFileStamps(?Uri $filterUri = null): \Generator 48 | { 49 | return array_merge( 50 | yield $this->secondaryStorage->getFileStamps($filterUri), 51 | yield $this->primaryStorage->getFileStamps($filterUri) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/Storage/IndexStorage.php: -------------------------------------------------------------------------------- 1 | string URI => ?string stamp 20 | */ 21 | public function getFileStamps(?Uri $filterUri = null): \Generator; 22 | } 23 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/Storage/MergedStorage.php: -------------------------------------------------------------------------------- 1 | innerStorage = $innerStorage; 24 | } 25 | 26 | public function search(Query $query): \Generator 27 | { 28 | $result = []; 29 | 30 | foreach ($this->innerStorage as $storage) { 31 | $result = array_merge($result, yield $storage->search($query)); 32 | } 33 | 34 | return $result; 35 | } 36 | 37 | public function getFileStamps(?Uri $filterUri = null): \Generator 38 | { 39 | $result = []; 40 | 41 | foreach ($this->innerStorage as $storage) { 42 | $result = array_merge($result, yield $storage->getFileStamps($filterUri)); 43 | } 44 | 45 | return $result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Index/Storage/WritableIndexStorage.php: -------------------------------------------------------------------------------- 1 | [string $fileType, string $stamp] 14 | */ 15 | public function list(Uri $uri, array $filters, ?Uri $baseUri = null): \Generator; 16 | } 17 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Io/FileLister/GlobFileFilter.php: -------------------------------------------------------------------------------- 1 | glob = $glob; 29 | $this->fileType = $fileType; 30 | $this->action = $action; 31 | } 32 | 33 | public function filter(string $uri, string $baseUri): int 34 | { 35 | if (Glob::match($uri, Path::join($baseUri, $this->glob))) { 36 | return $this->action; 37 | } 38 | 39 | return self::ABSTAIN; 40 | } 41 | 42 | public function getFileType(): string 43 | { 44 | return $this->fileType; 45 | } 46 | 47 | public function enterDirectory(string $uri, string $baseUri): int 48 | { 49 | $dir = Glob::getBasePath(Path::join($baseUri, $this->glob)); 50 | if ($dir === $uri || StringUtils::startsWith($uri, $dir . '/') || StringUtils::startsWith($dir, $uri . '/')) { 51 | return self::ACCEPT; 52 | } 53 | 54 | return self::ABSTAIN; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Io/FileLister/GlobRejectDirectoryFilter.php: -------------------------------------------------------------------------------- 1 | glob = $glob; 18 | } 19 | 20 | public function filter(string $uri, string $baseUri): int 21 | { 22 | if (Glob::match($uri, Path::join($baseUri, $this->glob, '**/*'))) { 23 | return self::REJECT; 24 | } 25 | 26 | return self::ABSTAIN; 27 | } 28 | 29 | public function getFileType(): string 30 | { 31 | return ''; 32 | } 33 | 34 | public function enterDirectory(string $uri, string $baseUri): int 35 | { 36 | if (Glob::match($uri, Path::join($baseUri, $this->glob . '{,/**/*}'))) { 37 | return self::REJECT; 38 | } 39 | 40 | return self::ABSTAIN; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Io/FileReader.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 22 | } 23 | 24 | public function onClose(Document $document): \Generator 25 | { 26 | yield Recoil::sleep(2.0); 27 | yield $this->eventDispatcher->dispatch(OnFileChange::class, [$document->getUri()]); 28 | } 29 | 30 | public function isAvailable(): bool 31 | { 32 | return true; 33 | } 34 | 35 | public function start(): \Generator 36 | { 37 | return; 38 | yield; 39 | } 40 | 41 | public function stop(): \Generator 42 | { 43 | return; 44 | yield; 45 | } 46 | 47 | public function addDirectory(Uri $uri): \Generator 48 | { 49 | return; 50 | yield; 51 | } 52 | 53 | public function removeDirectory(Uri $uri): \Generator 54 | { 55 | return; 56 | yield; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Io/FileWatcher/FileChangeDeduplicator.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private $uris = []; 26 | 27 | /** 28 | * @var bool 29 | */ 30 | private $accumulating = false; 31 | 32 | public function __construct(EventDispatcher $eventDispatcher, float $accumulateTime) 33 | { 34 | $this->eventDispatcher = $eventDispatcher; 35 | $this->accumulateTime = $accumulateTime; 36 | } 37 | 38 | /** 39 | * @param Uri[] $uris 40 | */ 41 | public function dispatch(array $uris): \Generator 42 | { 43 | foreach ($uris as $uri) { 44 | $this->uris[$uri->getNormalizedWithSlash()] = $uri; 45 | } 46 | 47 | if (!$this->accumulating) { 48 | $this->accumulating = true; 49 | 50 | try { 51 | yield Recoil::sleep($this->accumulateTime); 52 | $this->deduplicate(); 53 | yield $this->eventDispatcher->dispatch(OnFileChange::class, array_values($this->uris)); 54 | } finally { 55 | $this->uris = []; 56 | $this->accumulating = false; 57 | } 58 | } 59 | } 60 | 61 | private function deduplicate(): void 62 | { 63 | ksort($this->uris); 64 | /** @var Uri|null $prevUri */ 65 | $prevUri = null; 66 | foreach ($this->uris as $key => $uri) { 67 | if ($prevUri !== null && ($prevUri->equals($uri) || $prevUri->isParentOf($uri))) { 68 | unset($this->uris[$key]); 69 | } else { 70 | $prevUri = $uri; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Io/FileWatcher/FileWatcher.php: -------------------------------------------------------------------------------- 1 | throttler = new Throttler(self::MAX_CONCURRENT); 24 | } 25 | 26 | public function read(Uri $uri): \Generator 27 | { 28 | $job = function () use ($uri): \Generator { 29 | $file = false; 30 | 31 | try { 32 | $file = @fopen($uri->getFilesystemPath(), 'r'); 33 | if ($file === false) { 34 | throw new IoException("Can't open file $uri"); 35 | } 36 | 37 | stream_set_blocking($file, false); 38 | 39 | $content = yield Recoil::read($file, self::MAX_SIZE + 1, self::MAX_SIZE + 1); 40 | if (strlen($content) > self::MAX_SIZE) { 41 | throw new IoException("File size limit exceeded for $uri"); 42 | } 43 | } finally { 44 | if (is_resource($file)) { 45 | @fclose($file); 46 | } 47 | } 48 | 49 | return $content; 50 | }; 51 | 52 | return yield $this->throttler->run($job); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Logger/ClientLogger.php: -------------------------------------------------------------------------------- 1 | MessageType::ERROR, 17 | LogLevel::ALERT => MessageType::ERROR, 18 | LogLevel::CRITICAL => MessageType::ERROR, 19 | LogLevel::ERROR => MessageType::ERROR, 20 | LogLevel::WARNING => MessageType::WARNING, 21 | LogLevel::NOTICE => MessageType::WARNING, 22 | LogLevel::INFO => MessageType::INFO, 23 | LogLevel::DEBUG => MessageType::LOG, 24 | ]; 25 | 26 | /** 27 | * @var MessageFeature 28 | */ 29 | private $messageFeature; 30 | 31 | /** 32 | * @var Kernel 33 | */ 34 | private $kernel; 35 | 36 | public function __construct(MessageFeature $messageFeature, Kernel $kernel) 37 | { 38 | $this->messageFeature = $messageFeature; 39 | $this->kernel = $kernel; 40 | } 41 | 42 | public function log($level, $message, array $context = []) 43 | { 44 | $this->kernel->execute(function () use ($level, $message, $context) { 45 | yield $this->messageFeature->logMessage( 46 | static::LEVEL_MAP[$level], 47 | trim($this->interpolate($message, $context) . "\n" . ($context['exception'] ?? '')) 48 | ); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Logger/CompositeLogger.php: -------------------------------------------------------------------------------- 1 | loggers as $logger) { 18 | $logger->log($level, $message, $context); 19 | } 20 | } 21 | 22 | public function add(LoggerInterface $logger): self 23 | { 24 | $this->loggers[] = $logger; 25 | 26 | return $this; 27 | } 28 | 29 | public function clear(): self 30 | { 31 | $this->loggers = []; 32 | 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Logger/LevelFilteringLogger.php: -------------------------------------------------------------------------------- 1 | 8, 13 | LogLevel::ALERT => 7, 14 | LogLevel::CRITICAL => 6, 15 | LogLevel::ERROR => 5, 16 | LogLevel::WARNING => 4, 17 | LogLevel::NOTICE => 3, 18 | LogLevel::INFO => 2, 19 | LogLevel::DEBUG => 1, 20 | ]; 21 | 22 | /** 23 | * @var LoggerInterface 24 | */ 25 | private $inner; 26 | 27 | /** 28 | * @var int 29 | */ 30 | private $levelNumber; 31 | 32 | public function __construct(LoggerInterface $inner, string $level) 33 | { 34 | $this->inner = $inner; 35 | $this->levelNumber = $this->getLevelNumber($level); 36 | } 37 | 38 | private function getLevelNumber(string $level): int 39 | { 40 | return self::LEVEL_NUMBER[$level] ?? self::LEVEL_NUMBER[LogLevel::DEBUG]; 41 | } 42 | 43 | public function log($level, $message, array $context = []) 44 | { 45 | if ($this->getLevelNumber($level) >= $this->levelNumber) { 46 | $this->inner->log($level, $message, $context); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Logger/LoggerTrait.php: -------------------------------------------------------------------------------- 1 | $val) { 11 | if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { 12 | $replace['{' . $key . '}'] = $val; 13 | } 14 | } 15 | 16 | return strtr($message, $replace); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Logger/StreamLogger.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 22 | } 23 | 24 | public function log($level, $message, array $context = []) 25 | { 26 | $context['date'] = date(\DateTime::ATOM); 27 | $context['pid'] = (string)getmypid(); 28 | $context['level'] = strtoupper($level); 29 | $context['exception'] = isset($context['exception']) ? strtr((string)$context['exception'], ["\n" => "\n "]) : ''; 30 | 31 | fwrite($this->stream, trim($this->interpolate("{date} pid={pid} {level} $message\n {exception}", $context)) . "\n"); 32 | fflush($this->stream); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Mapper/UriMapper.php: -------------------------------------------------------------------------------- 1 | getMessage()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Plugin.php: -------------------------------------------------------------------------------- 1 | get()` or otherwize freeze the 13 | * container. 14 | */ 15 | public function configureContainer(Container $container, array $options): void 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/PluginFinder.php: -------------------------------------------------------------------------------- 1 | methodRegistry = $methodRegistry; 45 | $this->methodProviders = $methodProviders; 46 | $this->logger = $logger; 47 | $this->clientLogger = $clientLogger; 48 | } 49 | 50 | public function onStart(array $options): \Generator 51 | { 52 | if ($this->methodRegistry instanceof SimpleMethodRegistry) { 53 | foreach ($this->methodProviders as $provider) { 54 | $this->methodRegistry->addProvider($provider); 55 | } 56 | } 57 | 58 | if ($this->logger instanceof CompositeLogger) { 59 | if ($options['log.client'] ?? false) { 60 | $this->logger->add($this->clientLogger); 61 | } 62 | } 63 | 64 | return; 65 | yield; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Transport/RunnableTransport.php: -------------------------------------------------------------------------------- 1 | send($args); 30 | }; 31 | 32 | $errorListener = function ($error) use ($strand, &$removeListeners) { 33 | $removeListeners(); 34 | $strand->throw($error); 35 | }; 36 | 37 | $removeListeners = function () use ($emitter, $events, $errorEvents, $listener, $errorListener) { 38 | foreach ($events as $event) { 39 | $emitter->removeListener($event, $listener); 40 | } 41 | foreach ($errorEvents as $event) { 42 | $emitter->removeListener($event, $errorListener); 43 | } 44 | }; 45 | 46 | foreach ($events as $event) { 47 | $emitter->on($event, $listener); 48 | } 49 | foreach ($errorEvents as $event) { 50 | $emitter->on($event, $errorListener); 51 | } 52 | }); 53 | } 54 | 55 | // @codeCoverageIgnoreStart 56 | private function __construct() 57 | { 58 | } 59 | 60 | // @codeCoverageIgnoreEnd 61 | } 62 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/FuzzyMatcher.php: -------------------------------------------------------------------------------- 1 | data[$key] ?? null; 20 | } 21 | 22 | public function set(string $key, $data): self 23 | { 24 | $this->data[$key] = $data; 25 | 26 | return $this; 27 | } 28 | 29 | public function clear(): self 30 | { 31 | $this->data = []; 32 | 33 | return $this; 34 | } 35 | 36 | public function isClosed(): bool 37 | { 38 | return $this->closed; 39 | } 40 | 41 | public function close(): void 42 | { 43 | $this->closed = true; 44 | $this->clear(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/NestedKernelsSyncAsync.php: -------------------------------------------------------------------------------- 1 | kernelFactory = $kernelFactory; 27 | } 28 | 29 | public function callSync( 30 | callable $syncCallable, 31 | array $args = [], 32 | ?callable $resumeCallback = null, 33 | ?callable $pauseCallback = null 34 | ) { 35 | $context = new SyncCallContext(); 36 | $context->resumeCallback = $resumeCallback; 37 | $context->pauseCallback = $pauseCallback; 38 | 39 | $this->syncStack[] = $context; 40 | $context->resume(); 41 | 42 | try { 43 | $result = $syncCallable(...$args); 44 | } finally { 45 | $context->pause(); 46 | array_pop($this->syncStack); 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | public function callAsync(\Generator $coroutine) 53 | { 54 | assert(!empty($this->syncStack)); 55 | $context = $this->syncStack[count($this->syncStack) - 1]; 56 | $context->pause(); 57 | 58 | $kernel = $this->cachedKernel ?? ($this->kernelFactory)(); 59 | $this->cachedKernel = null; 60 | 61 | try { 62 | $result = $kernel->start($coroutine); 63 | } finally { 64 | $context->resume(); 65 | $this->cachedKernel = $kernel; 66 | } 67 | 68 | return $result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/Platform.php: -------------------------------------------------------------------------------- 1 | setPriority($priority); 18 | } 19 | 20 | yield; 21 | } 22 | 23 | public static function interactive(int $bonus = 0): \Generator 24 | { 25 | return self::set(self::INTERACTIVE + $bonus); 26 | } 27 | 28 | public static function foreground(int $bonus = 0): \Generator 29 | { 30 | return self::set(self::FOREGROUND + $bonus); 31 | } 32 | 33 | public static function background(int $bonus = 0): \Generator 34 | { 35 | return self::set(self::BACKGROUND + $bonus); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/PriorityStrand.php: -------------------------------------------------------------------------------- 1 | priority; 20 | } 21 | 22 | /** 23 | * @return $this 24 | */ 25 | public function setPriority(int $priority): self 26 | { 27 | $this->priority = $priority; 28 | 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/ScheduledReactKernel.php: -------------------------------------------------------------------------------- 1 | eventLoop = $eventLoop; 37 | $this->api = $api; 38 | $this->scheduler = $scheduler; 39 | $this->panicExceptions = new SplQueue(); 40 | } 41 | 42 | public static function create(?LoopInterface $eventLoop = null, ?Scheduler $scheduler = null): self 43 | { 44 | if ($eventLoop === null) { 45 | $eventLoop = Factory::create(); 46 | } 47 | 48 | if ($scheduler === null) { 49 | $scheduler = new ReactScheduler($eventLoop); 50 | } 51 | 52 | return new self( 53 | $eventLoop, 54 | new ScheduledApi(new ReactApi($eventLoop), $scheduler), 55 | $scheduler 56 | ); 57 | } 58 | 59 | public function execute($coroutine): Strand 60 | { 61 | $strand = new PriorityStrand($this, $this->api, $this->nextId++, $coroutine); 62 | $this->scheduler->scheduleStart($strand); 63 | 64 | return $strand; 65 | } 66 | 67 | public function stop() 68 | { 69 | /** @var int $state */ 70 | $state = &$this->state; // To override wrong annotation in KernelTrait 71 | if ($state === KernelState::RUNNING) { 72 | $state = KernelState::STOPPING; 73 | $this->eventLoop->stop(); 74 | } 75 | } 76 | 77 | protected function loop() 78 | { 79 | $this->eventLoop->run(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/PriorityKernel/Scheduler.php: -------------------------------------------------------------------------------- 1 | start = $this->now(); 20 | $this->scale = $scale; 21 | } 22 | 23 | public function getSeconds(): float 24 | { 25 | return $this->now() - $this->start; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | $seconds = $this->getSeconds(); 31 | $minutes = (int)($seconds / 60); 32 | $seconds = fmod($seconds, 60); 33 | $hours = (int)($minutes / 60); 34 | $minutes = $minutes % 60; 35 | 36 | $result = ''; 37 | if ($hours !== 0) { 38 | $result .= $hours . 'h'; 39 | if ($minutes < 10) { 40 | $result .= '0'; 41 | } 42 | } 43 | if ($hours !== 0 || $minutes !== 0) { 44 | $result .= $minutes . 'm'; 45 | if ($seconds < 10) { 46 | $result .= '0'; 47 | } 48 | } 49 | $result .= sprintf("%.{$this->scale}fs", $seconds); 50 | 51 | return $result; 52 | } 53 | 54 | private function now(): float 55 | { 56 | return microtime(true); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/StringTemplate.php: -------------------------------------------------------------------------------- 1 | template = $template; 15 | } 16 | 17 | /** 18 | * @param array $variables 19 | * 20 | * @resolve string 21 | */ 22 | public function render(array $variables): \Generator 23 | { 24 | $curlyVariables = []; 25 | foreach ($variables as $name => $value) { 26 | $curlyVariables['{{' . $name . '}}'] = $value; 27 | } 28 | 29 | return strtr($this->template, $curlyVariables); 30 | yield; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/StringUtils.php: -------------------------------------------------------------------------------- 1 | $maxLength) { 20 | return substr($str, 0, $maxLength - 3) . '...'; 21 | } 22 | 23 | return $str; 24 | } 25 | 26 | public static function getShortName(string $fullName): string 27 | { 28 | $parts = explode('\\', $fullName); 29 | 30 | return $parts[count($parts) - 1]; 31 | } 32 | 33 | public static function getNamespace(string $fullName): string 34 | { 35 | $parts = explode('\\', $fullName); 36 | array_pop($parts); 37 | 38 | return implode('\\', $parts); 39 | } 40 | 41 | /** 42 | * @param array $matches 43 | */ 44 | public static function match(string $regex, string $str, array &$matches = null): bool 45 | { 46 | $result = preg_match($regex, $str, $matches); 47 | 48 | if ($result === false) { 49 | throw new \InvalidArgumentException(__METHOD__ . '(): invalid argument'); 50 | } 51 | 52 | return (bool)$result; 53 | } 54 | 55 | /** 56 | * @param string|\Closure $replacement 57 | */ 58 | public static function replace(string $regex, $replacement, string $str): string 59 | { 60 | if ($replacement instanceof \Closure) { 61 | $result = preg_replace_callback($regex, $replacement, $str); 62 | } else { 63 | $result = preg_replace($regex, $replacement, $str); 64 | } 65 | 66 | if ($result === null) { 67 | throw new \InvalidArgumentException(__METHOD__ . '(): invalid argument'); 68 | } 69 | 70 | return $result; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/SyncAsync.php: -------------------------------------------------------------------------------- 1 | resumeCallback !== null) { 20 | ($this->resumeCallback)(); 21 | } 22 | } 23 | 24 | public function pause(): void 25 | { 26 | if ($this->pauseCallback !== null) { 27 | ($this->pauseCallback)(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/Template.php: -------------------------------------------------------------------------------- 1 | $variables 9 | * 10 | * @resolve string 11 | */ 12 | public function render(array $variables): \Generator; 13 | } 14 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Server/Utils/Throttler.php: -------------------------------------------------------------------------------- 1 | maxConcurrentJobs = $maxConcurrentJobs; 28 | $this->queue = new \SplQueue(); 29 | } 30 | 31 | /** 32 | * @param mixed $job Coroutine to run. 33 | */ 34 | public function run($job): \Generator 35 | { 36 | while ($this->inProgressJobs >= $this->maxConcurrentJobs) { 37 | $strand = yield Recoil::strand(); 38 | $this->queue->enqueue($strand); 39 | yield Recoil::suspend(); 40 | } 41 | 42 | $result = null; 43 | $this->inProgressJobs++; 44 | 45 | try { 46 | $result = yield $job; 47 | } finally { 48 | $this->inProgressJobs--; 49 | if (!$this->queue->isEmpty()) { 50 | /** @var Strand $next */ 51 | $next = $this->queue->dequeue(); 52 | yield Recoil::execute(function () use ($next): \Generator { 53 | $next->send(); 54 | 55 | return; 56 | yield; 57 | }); 58 | } 59 | } 60 | 61 | return $result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/Symfony/Container/ServiceMapUpdater.php: -------------------------------------------------------------------------------- 1 | serviceMap = $serviceMap; 29 | $this->serviceMapWatcher = $serviceMapWatcher; 30 | $this->reflectionProperty = (new \ReflectionClass(ServiceMap::class))->getProperty('services'); 31 | $this->reflectionProperty->setAccessible(true); 32 | } 33 | 34 | public function setProject(?Project $project): void 35 | { 36 | $newServiceMap = null; 37 | if ($project !== null) { 38 | $newServiceMap = $this->serviceMapWatcher->getServiceMap($project); 39 | } 40 | 41 | $this->setServiceMap($newServiceMap); 42 | } 43 | 44 | private function setServiceMap(?ServiceMap $newServiceMap): void 45 | { 46 | $services = []; 47 | if ($newServiceMap !== null) { 48 | $services = $this->reflectionProperty->getValue($newServiceMap); 49 | } 50 | 51 | $this->reflectionProperty->setValue($this->serviceMap, $services); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tsufeki/Tenkawa/WebMozartAssert/WebMozartAssertPlugin.php: -------------------------------------------------------------------------------- 1 | setClass(AssertTypeSpecifyingExtension::class, StaticMethodTypeSpecifyingExtension::class, true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/DummyTransportPair.php: -------------------------------------------------------------------------------- 1 | observer = $observer; 23 | } 24 | 25 | public function send(string $message): \Generator 26 | { 27 | yield $this->other->observer->receive($message); 28 | } 29 | 30 | public function run(): \Generator 31 | { 32 | return; 33 | yield; 34 | } 35 | 36 | /** 37 | * @return RunnableTransport[] 38 | */ 39 | public static function create(): array 40 | { 41 | $ends = [new static(), new static()]; 42 | 43 | $ends[0]->other = $ends[1]; 44 | $ends[1]->other = $ends[0]; 45 | 46 | return $ends; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/Server/Event/EventDispatcherTest.php: -------------------------------------------------------------------------------- 1 | data = $data; 24 | 25 | return; 26 | yield; 27 | } 28 | }; 29 | 30 | $container = $this->createMock(Container::class); 31 | $container 32 | ->expects($this->once()) 33 | ->method('getOrDefault') 34 | ->with($this->identicalTo('OnEvent'), $this->identicalTo([])) 35 | ->willReturn([$listener]); 36 | 37 | $data = new \stdClass(); 38 | $dispatcher = new EventDispatcher($container); 39 | yield $dispatcher->dispatch('OnEvent', $data); 40 | yield; 41 | yield; 42 | yield; 43 | 44 | $this->assertSame($data, $listener->data); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/Server/Index/Storage/MemoryStorageTest.php: -------------------------------------------------------------------------------- 1 | emit('evt', [42]); 25 | })(), 26 | ]; 27 | 28 | $this->assertSame([42], $actual); 29 | }); 30 | } 31 | 32 | public function test_first_error() 33 | { 34 | $this->expectException(\RuntimeException::class); 35 | 36 | ReactKernel::start(function () { 37 | $emitter = new EventEmitter(); 38 | 39 | yield [ 40 | Event::first($emitter, [], ['err']), 41 | (function () use ($emitter) { 42 | yield; 43 | $emitter->emit('err', [new \RuntimeException()]); 44 | })(), 45 | ]; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/Server/Utils/StringUtilsTest.php: -------------------------------------------------------------------------------- 1 | assertSame($result, StringUtils::startsWith($haystack, $needle)); 19 | } 20 | 21 | public function data_starts_with(): array 22 | { 23 | return [ 24 | ['', '', true], 25 | ['abc', '', true], 26 | ['', 'abc', false], 27 | ['abc', 'a', true], 28 | ['a', 'abc', false], 29 | ['abc', 'ab', true], 30 | ['ab', 'abc', false], 31 | ['abc', 'abc', true], 32 | ['abc', 'abcd', false], 33 | ['abc', 'Abc', false], 34 | ['abc', 'abD', false], 35 | ['abc', 'aB', false], 36 | ]; 37 | } 38 | 39 | /** 40 | * @dataProvider data_ends_with 41 | */ 42 | public function test_ends_with($haystack, $needle, $result) 43 | { 44 | $this->assertSame($result, StringUtils::endsWith($haystack, $needle)); 45 | } 46 | 47 | public function data_ends_with(): array 48 | { 49 | return [ 50 | ['', '', true], 51 | ['abc', '', true], 52 | ['', 'abc', false], 53 | ['abc', 'c', true], 54 | ['c', 'abc', false], 55 | ['abc', 'bc', true], 56 | ['bc', 'abc', false], 57 | ['a', 'abc', false], 58 | ['abc', 'abc', true], 59 | ['abc', 'zabc', false], 60 | ['abc', 'Abc', false], 61 | ['abc', 'abD', false], 62 | ['abc', 'Bc', false], 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/Server/Utils/SyncAsyncTest.php: -------------------------------------------------------------------------------- 1 | execute(function () use ($sa) { 21 | $result = $sa->callSync(function () use ($sa) { 22 | return 'foo' . $sa->callAsync((function () { 23 | yield; 24 | 25 | return 'bar'; 26 | })()); 27 | }); 28 | yield; 29 | 30 | $this->assertSame('foobar', $result); 31 | }); 32 | $kernel->run(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/TestCase.php: -------------------------------------------------------------------------------- 1 | kernel = ScheduledReactKernel::create(); 20 | } 21 | 22 | protected function async($coroutine) 23 | { 24 | $result = null; 25 | $exception = null; 26 | 27 | $this->kernel->execute(function () use ($coroutine, &$result, &$exception) { 28 | try { 29 | $result = yield $coroutine; 30 | } catch (\Throwable $e) { 31 | $exception = $e; 32 | } 33 | }); 34 | 35 | $this->kernel->run(); 36 | 37 | if ($exception !== null) { 38 | throw $exception; 39 | } 40 | 41 | return $result; 42 | } 43 | 44 | public function assertJsonEquivalent($expected, $actual) 45 | { 46 | $this->assertJsonStringEqualsJsonString(json_encode($expected) ?: '', json_encode($actual) ?: ''); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Tsufeki/Tenkawa/fixtures/Foo/SelfCompletion.php: -------------------------------------------------------------------------------- 1 |