| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 280ca4f commit 2fa45fa
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1328,6 +1328,19 @@ If applicable diagnostics should be "outsourced" to a different process | |||
| 1328 | 1328 | **Default**: ``true`` | |
| 1329 | 1329 | ||
| 1330 | 1330 | ||
| 1331 | + .. _param_language_server.code_action_outsource: | ||
| 1332 | + | ||
| 1333 | + | ||
| 1334 | + ``language_server.code_action_outsource`` | ||
| 1335 | + """"""""""""""""""""""""""""""""""""""""" | ||
| 1336 | + | ||
| 1337 | + | ||
| 1338 | + Code actions will be "outsourced" to a different process | ||
| 1339 | + | ||
| 1340 | + | ||
| 1341 | + **Default**: ``true`` | ||
| 1342 | + | ||
| 1343 | + | ||
| 1331 | 1344 | .. _param_language_server.diagnostic_exclude_paths: | |
| 1332 | 1345 | ||
| 1333 | 1346 | ||
@@ -1462,7 +1475,7 @@ Wait this amount of time (in milliseconds) after a shutdown request before self- | |||
| 1462 | 1475 | """""""""""""""""""""""""""""""""""""""""""""""" | |
| 1463 | 1476 | ||
| 1464 | 1477 | ||
| 1465 | - Kill the diagnostics process if it outlives this timeout | ||
| 1478 | + Kill the diagnostics or code action processes if they outlive this timeout | ||
| 1466 | 1479 | ||
| 1467 | 1480 | ||
| 1468 | 1481 | **Default**: ``5`` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,126 @@ | |||
| 1 | + <?php | ||
| 2 | + | ||
| 3 | + namespace Phpactor\Extension\LanguageServer\CodeAction; | ||
| 4 | + | ||
| 5 | + use Amp\CancellationToken; | ||
| 6 | + use Amp\Process\Process; | ||
| 7 | + use Amp\Process\ProcessException; | ||
| 8 | + use Amp\Promise; | ||
| 9 | + use Phpactor\Amp\Process\ProcessUtil; | ||
| 10 | + use Phpactor\Extension\WorseReflection\WorseReflectionExtension; | ||
| 11 | + use Phpactor\LanguageServerProtocol\CodeAction; | ||
| 12 | + use Phpactor\LanguageServerProtocol\CodeActionContext; | ||
| 13 | + use Phpactor\LanguageServerProtocol\CodeActionParams; | ||
| 14 | + use Phpactor\LanguageServerProtocol\Range; | ||
| 15 | + use Phpactor\LanguageServerProtocol\TextDocumentItem; | ||
| 16 | + use Phpactor\LanguageServer\Core\CodeAction\CodeActionProvider; | ||
| 17 | + use Phpactor\LanguageServer\Core\Server\ClientApi; | ||
| 18 | + use Phpactor\LanguageServer\Test\ProtocolFactory; | ||
| 19 | + use Psr\Log\LoggerInterface; | ||
| 20 | + use RuntimeException; | ||
| 21 | + use function Amp\ByteStream\buffer; | ||
| 22 | + use function Amp\asyncCall; | ||
| 23 | + use function Amp\call; | ||
| 24 | + use function Amp\delay; | ||
| 25 | + | ||
| 26 | + class OutsourcedCodeActionProvider implements CodeActionProvider | ||
| 27 | + { | ||
| 28 | + /** | ||
| 29 | + * @param list<string> $command | ||
| 30 | + */ | ||
| 31 | + public function __construct( | ||
| 32 | + private array $command, | ||
| 33 | + private string $cwd, | ||
| 34 | + private LoggerInterface $logger, | ||
| 35 | + private ClientApi $client, | ||
| 36 | + private CodeActionProvider $providerInfo, | ||
| 37 | + private int $timeout = 5, | ||
| 38 | + ) { | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise | ||
| 42 | + { | ||
| 43 | + return call(function () use ($textDocument, $range, $cancel) { | ||
| 44 | + $process = new Process(array_merge([ | ||
| 45 | + PHP_BINARY | ||
| 46 | + ], $this->command, [ | ||
| 47 | + json_encode(new CodeActionParams( | ||
| 48 | + ProtocolFactory::textDocumentIdentifier($textDocument->uri), | ||
| 49 | + $range, | ||
| 50 | + new CodeActionContext([]), | ||
| 51 | + ), JSON_THROW_ON_ERROR), | ||
| 52 | + sprintf('--config-extra=%s', sprintf('{"%s": false}', WorseReflectionExtension::PARAM_ENABLE_CONTEXT_LOCATION)) | ||
| 53 | + ]), $this->cwd); | ||
| 54 | + | ||
| 55 | + /** @var int $pid */ | ||
| 56 | + $pid = yield $process->start(); | ||
| 57 | + | ||
| 58 | + ProcessUtil::killAfter($this->logger, $process, $this->timeout); | ||
| 59 | + | ||
| 60 | + $stdin = $process->getStdin(); | ||
| 61 | + | ||
| 62 | + asyncCall(function () use ($process, $cancel, $pid) { | ||
| 63 | + while ($process->isRunning()) { | ||
| 64 | + if ($cancel->isRequested()) { | ||
| 65 | + $process->kill(); | ||
| 66 | + $this->logger->info(sprintf( | ||
| 67 | + 'Killing process code-action process "%s" as requested', | ||
| 68 | + $pid, | ||
| 69 | + )); | ||
| 70 | + } | ||
| 71 | + yield delay(500); | ||
| 72 | + } | ||
| 73 | + }); | ||
| 74 | + | ||
| 75 | + yield $stdin->write($textDocument->text); | ||
| 76 | + | ||
| 77 | + $stdin->close(); | ||
| 78 | + | ||
| 79 | + /** @var string $json */ | ||
| 80 | + $json = yield buffer($process->getStdout()); | ||
| 81 | + | ||
| 82 | + try { | ||
| 83 | + /** @var int $exitCode */ | ||
| 84 | + $exitCode = yield $process->join(); | ||
| 85 | + } catch (ProcessException $e) { | ||
| 86 | + $this->client->window()->showMessage()->warning(sprintf( | ||
| 87 | + 'Code action took too long to analyse this file (timed-out after %s seconds)', | ||
| 88 | + $this->timeout, | ||
| 89 | + )); | ||
| 90 | + return []; | ||
| 91 | + } | ||
| 92 | + if ($exitCode !== 0) { | ||
| 93 | + /** @var string $stderr */ | ||
| 94 | + $stderr = yield buffer($process->getStderr()); | ||
| 95 | + | ||
| 96 | + throw new RuntimeException(sprintf( | ||
| 97 | + 'Phpactor code-action process exited with code "%s": %s', | ||
| 98 | + $exitCode, | ||
| 99 | + $stderr | ||
| 100 | + )); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + $array = json_decode($json, true); | ||
| 104 | + | ||
| 105 | + if (!is_array($array)) { | ||
| 106 | + throw new RuntimeException(sprintf( | ||
| 107 | + 'Could not decode JSON: %s', | ||
| 108 | + $json | ||
| 109 | + )); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + /** @phpstan-ignore-next-line */ | ||
| 113 | + return array_map(fn (array $codeAction) => CodeAction::fromArray($codeAction), $array); | ||
| 114 | + }); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + public function kinds(): array | ||
| 118 | + { | ||
| 119 | + return $this->providerInfo->kinds(); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + public function describe(): string | ||
| 123 | + { | ||
| 124 | + return sprintf('outsourced: %s', $this->providerInfo->describe()); | ||
| 125 | + } | ||
| 126 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,44 @@ | |||
| 1 | + <?php | ||
| 2 | + | ||
| 3 | + namespace Phpactor\Extension\LanguageServer\CodeAction; | ||
| 4 | + | ||
| 5 | + use Amp\CancellationToken; | ||
| 6 | + use Amp\CancellationTokenSource; | ||
| 7 | + use Amp\CombinedCancellationToken; | ||
| 8 | + use Amp\Promise; | ||
| 9 | + use Phpactor\LanguageServerProtocol\Range; | ||
| 10 | + use Phpactor\LanguageServerProtocol\TextDocumentItem; | ||
| 11 | + use Phpactor\LanguageServer\Core\CodeAction\CodeActionProvider; | ||
| 12 | + | ||
| 13 | + class ThereCanOnlyBeOneCodeActionProvider implements CodeActionProvider | ||
| 14 | + { | ||
| 15 | + private ?CancellationTokenSource $cancel = null; | ||
| 16 | + | ||
| 17 | + public function __construct(private CodeActionProvider $inner) | ||
| 18 | + { | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + public function provideActionsFor(TextDocumentItem $textDocument, Range $range, CancellationToken $cancel): Promise | ||
| 22 | + { | ||
| 23 | + if ($this->cancel) { | ||
| 24 | + $this->cancel->cancel(); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + $this->cancel = new CancellationTokenSource(); | ||
| 28 | + | ||
| 29 | + return $this->inner->provideActionsFor($textDocument, $range, new CombinedCancellationToken( | ||
| 30 | + $cancel, | ||
| 31 | + $this->cancel->getToken(), | ||
| 32 | + )); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public function kinds(): array | ||
| 36 | + { | ||
| 37 | + return $this->inner->kinds(); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + public function describe(): string | ||
| 41 | + { | ||
| 42 | + return $this->inner->describe(); | ||
| 43 | + } | ||
| 44 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,7 +15,7 @@ final class TolerantCodeActionProvider implements CodeActionProvider | |||
| 15 | 15 | { | |
| 16 | 16 | public function __construct( | |
| 17 | 17 | private CodeActionProvider $provider, | |
| 18 | - private ClientApi $client | ||
| 18 | + private ?ClientApi $client | ||
| 19 | 19 | ) { | |
| 20 | 20 | } | |
| 21 | 21 | ||
@@ -25,13 +25,19 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range, | |||
| 25 | 25 | try { | |
| 26 | 26 | return yield $this->provider->provideActionsFor($textDocument, $range, $cancel); | |
| 27 | 27 | } catch (Throwable $error) { | |
| 28 | - $this->client->window()->showMessage()->error(sprintf( | ||
| 29 | - 'Provider %s (%s) failed: %s', | ||
| 30 | - $this->provider::class, | ||
| 31 | - $this->provider->describe(), | ||
| 32 | - $error->getMessage(), | ||
| 33 | - )); | ||
| 34 | - return []; | ||
| 28 | + // if we are running in the main process the LS client API will be available | ||
| 29 | + if (null !== $this->client) { | ||
| 30 | + $this->client->window()->showMessage()->error(sprintf( | ||
| 31 | + 'Provider %s (%s) failed: %s', | ||
| 32 | + $this->provider::class, | ||
| 33 | + $this->provider->describe(), | ||
| 34 | + $error->getMessage(), | ||
| 35 | + )); | ||
| 36 | + return []; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + // otherwise we're probably running in a dedicated process, just throw an error and let it die. | ||
| 40 | + throw $error; | ||
| 35 | 41 | } | |
| 36 | 42 | }); | |
| 37 | 43 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,83 @@ | |||
| 1 | + <?php | ||
| 2 | + | ||
| 3 | + namespace Phpactor\Extension\LanguageServer\Command; | ||
| 4 | + | ||
| 5 | + use Amp\CancellationTokenSource; | ||
| 6 | + use Phpactor\Extension\LanguageServerBridge\Converter\TextDocumentConverter; | ||
| 7 | + use Phpactor\Extension\LanguageServerWorseReflection\Workspace\WorkspaceIndex; | ||
| 8 | + use Phpactor\LanguageServerProtocol\CodeActionParams; | ||
| 9 | + use Phpactor\LanguageServer\Core\CodeAction\CodeActionProvider; | ||
| 10 | + use Phpactor\LanguageServer\Test\ProtocolFactory; | ||
| 11 | + use RuntimeException; | ||
| 12 | + use Symfony\Component\Console\Command\Command; | ||
| 13 | + use Symfony\Component\Console\Input\InputArgument; | ||
| 14 | + use Symfony\Component\Console\Input\InputInterface; | ||
| 15 | + use Symfony\Component\Console\Output\OutputInterface; | ||
| 16 | + use function Amp\Promise\wait; | ||
| 17 | + | ||
| 18 | + class CodeActionsCommand extends Command | ||
| 19 | + { | ||
| 20 | + public const ARG_REQUEST = 'request'; | ||
| 21 | + public const NAME = 'language-server:code-actions'; | ||
| 22 | + | ||
| 23 | + public function __construct( | ||
| 24 | + private CodeActionProvider $provider, | ||
| 25 | + private WorkspaceIndex $workspace, | ||
| 26 | + ) { | ||
| 27 | + parent::__construct(); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + protected function configure(): void | ||
| 31 | + { | ||
| 32 | + $this->setDescription('Internal: resolve code-actions asynchronously'); | ||
| 33 | + $this->addArgument(self::ARG_REQUEST, InputArgument::REQUIRED, 'Code action LSP request'); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + protected function execute(InputInterface $input, OutputInterface $output): int | ||
| 37 | + { | ||
| 38 | + /** @var string $request */ | ||
| 39 | + $request = $input->getArgument(self::ARG_REQUEST); | ||
| 40 | + | ||
| 41 | + $array = json_decode($request, true, JSON_THROW_ON_ERROR); | ||
| 42 | + if (!is_array($array)) { | ||
| 43 | + throw new RuntimeException(sprintf( | ||
| 44 | + 'Expected json to decode to an array, got "%s"', | ||
| 45 | + get_debug_type($array) | ||
| 46 | + )); | ||
| 47 | + } | ||
| 48 | + /** @phpstan-ignore argument.type */ | ||
| 49 | + $request = CodeActionParams::fromArray($array); | ||
| 50 | + $textDocumentItem = ProtocolFactory::textDocumentItem($request->textDocument->uri, $this->stdin()); | ||
| 51 | + | ||
| 52 | + // update the in-memory worse reflection workspace index so that we | ||
| 53 | + // can locate the latest function and class definitions in this process. | ||
| 54 | + $this->workspace->index(TextDocumentConverter::fromLspTextItem($textDocumentItem)); | ||
| 55 | + | ||
| 56 | + $diagnostics = wait( | ||
| 57 | + $this->provider->provideActionsFor( | ||
| 58 | + $textDocumentItem, | ||
| 59 | + $request->range, | ||
| 60 | + (new CancellationTokenSource())->getToken() | ||
| 61 | + ) | ||
| 62 | + ); | ||
| 63 | + $decoded = json_encode($diagnostics); | ||
| 64 | + if (false === $decoded) { | ||
| 65 | + throw new RuntimeException( | ||
| 66 | + 'Could not encode diagnostics', | ||
| 67 | + ); | ||
| 68 | + } | ||
| 69 | + $output->write($decoded); | ||
| 70 | + return 0; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + private function stdin(): string | ||
| 74 | + { | ||
| 75 | + $in = ''; | ||
| 76 | + | ||
| 77 | + while (false !== $line = fgets(STDIN)) { | ||
| 78 | + $in .= $line; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + return $in; | ||
| 82 | + } | ||
| 83 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments