FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Gh 3042: No stacking code actions / code-action concurrency (#3048) · phpactor/phpactor@2fa45fa · GitHub

Commit 2fa45fa

Browse files
authored
Gh 3042: No stacking code actions / code-action concurrency (#3048)
- Ensure that only one code-action resolution happens at one time and that any previous operation is cancelled... - ... run the action in a separate process so that it's non-blocking (and can therefore also be cancelled).
1 parent 280ca4f commit 2fa45fa

9 files changed

Lines changed: 394 additions & 19 deletions

File tree

‎doc/reference/configuration.rst‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,19 @@ If applicable diagnostics should be "outsourced" to a different process
13281328
**Default**: ``true``
13291329

13301330

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+
13311344
.. _param_language_server.diagnostic_exclude_paths:
13321345

13331346

@@ -1462,7 +1475,7 @@ Wait this amount of time (in milliseconds) after a shutdown request before self-
14621475
""""""""""""""""""""""""""""""""""""""""""""""""
14631476

14641477

1465-
Kill the diagnostics process if it outlives this timeout
1478+
Kill the diagnostics or code action processes if they outlive this timeout
14661479

14671480

14681481
**Default**: ``5``
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
}

‎lib/Extension/LanguageServer/CodeAction/TolerantCodeActionProvider.php‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class TolerantCodeActionProvider implements CodeActionProvider
1515
{
1616
public function __construct(
1717
private CodeActionProvider $provider,
18-
private ClientApi $client
18+
private ?ClientApi $client
1919
) {
2020
}
2121

@@ -25,13 +25,19 @@ public function provideActionsFor(TextDocumentItem $textDocument, Range $range,
2525
try {
2626
return yield $this->provider->provideActionsFor($textDocument, $range, $cancel);
2727
} 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;
3541
}
3642
});
3743
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff 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+
}

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL