diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index f6525aacea6..6716f2024ac 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -238,7 +238,7 @@ function () { $this->textDocument = new ServerTextDocument( $this, $codebase, - $this->project_analyzer->onchange_line_limit + $this->project_analyzer ); } @@ -246,7 +246,7 @@ function () { $this->workspace = new ServerWorkspace( $this, $codebase, - $this->project_analyzer->onchange_line_limit + $this->project_analyzer ); } @@ -279,6 +279,8 @@ function () { // Support "Hover" $serverCapabilities->hoverProvider = true; // Support "Completion" + $serverCapabilities->codeActionProvider = true; + // Support "Code Actions" if ($this->project_analyzer->provide_completion) { $serverCapabilities->completionProvider = new CompletionOptions(); diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 0b613926a55..989f459b32d 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -17,14 +17,21 @@ use LanguageServerProtocol\TextDocumentContentChangeEvent; use LanguageServerProtocol\TextDocumentIdentifier; use LanguageServerProtocol\TextDocumentItem; +use LanguageServerProtocol\TextEdit; use LanguageServerProtocol\VersionedTextDocumentIdentifier; +use LanguageServerProtocol\WorkspaceEdit; use Psalm\Codebase; use Psalm\Exception\UnanalyzedFileException; +use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\LanguageServer\LanguageServer; +use Psalm\IssueBuffer; use UnexpectedValueException; +use function array_combine; +use function array_values; use function count; use function error_log; +use function preg_match; use function substr_count; /** @@ -42,17 +49,19 @@ class TextDocument */ protected $codebase; - /** @var ?int */ - protected $onchange_line_limit; + /** + * @var ProjectAnalyzer + */ + protected $project_analyzer; public function __construct( LanguageServer $server, Codebase $codebase, - ?int $onchange_line_limit + ProjectAnalyzer $project_analyzer ) { $this->server = $server; $this->codebase = $codebase; - $this->onchange_line_limit = $onchange_line_limit; + $this->project_analyzer = $project_analyzer; } /** @@ -116,7 +125,7 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ return; } - if ($this->onchange_line_limit === 0) { + if ($this->project_analyzer->onchange_line_limit === 0) { return; } @@ -126,8 +135,8 @@ public function didChange(VersionedTextDocumentIdentifier $textDocument, array $ throw new UnexpectedValueException('Not expecting partial diff'); } - if ($this->onchange_line_limit !== null) { - if (substr_count($new_content, "\n") > $this->onchange_line_limit) { + if ($this->project_analyzer->onchange_line_limit !== null) { + if (substr_count($new_content, "\n") > $this->project_analyzer->onchange_line_limit) { return; } } @@ -341,4 +350,89 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po $signature_information, ], 0, $argument_location[1])); } + + /** + * The code action request is sent from the client to the server to compute commands + * for a given text document and range. These commands are typically code fixes to + * either fix problems or to beautify/refactor code. + * + */ + public function codeAction(TextDocumentIdentifier $textDocument, Range $range): Promise + { + $file_path = LanguageServer::uriToPath($textDocument->uri); + if (!$this->codebase->file_provider->isOpen($file_path)) { + return new Success(null); + } + + $all_file_paths_to_analyze = [$file_path]; + $this->codebase->analyzer->addFilesToAnalyze( + array_combine($all_file_paths_to_analyze, $all_file_paths_to_analyze) + ); + $this->codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); + + $issues = IssueBuffer::clear(); + + if (empty($issues[$file_path])) { + return new Success(null); + } + + $file_contents = $this->codebase->getFileContents($file_path); + + $offsetStart = $range->start->toOffset($file_contents); + $offsetEnd = $range->end->toOffset($file_contents); + + $fixers = []; + foreach ($issues[$file_path] as $issue) { + if ($offsetStart === $issue->from && $offsetEnd === $issue->to) { + $snippetRange = new Range( + new Position($issue->line_from-1), + new Position($issue->line_to) + ); + + $indentation = ''; + if (preg_match('/^(\s*)/', $issue->snippet, $matches)) { + $indentation = $matches[1] ?? ''; + } + + + /** + * Suppress Psalm because ther are bugs in how + * LanguageServer's signature of WorkspaceEdit is declared: + * + * See: + * https://github.com/felixfbecker/php-language-server-protocol + * See: + * https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceEdit + * + * @psalm-suppress InvalidArgument + */ + $edit = new WorkspaceEdit([ + $textDocument->uri => [ + new TextEdit( + $snippetRange, + "{$indentation}/**\n". + "{$indentation} * @psalm-suppress {$issue->type}\n". + "{$indentation} */\n". + "{$issue->snippet}\n" + ) + ] + ]); + + //Suppress Ability + $fixers["suppress.{$issue->type}"] = [ + 'title' => "Suppress {$issue->type} for this line", + 'kind' => 'quickfix', + 'edit' => $edit + ]; + } + } + + if (empty($fixers)) { + return new Success(null); + } + + return new Success( + array_values($fixers) + ); + } } diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 3d51d005c95..c1bc2bd7073 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -7,6 +7,7 @@ use LanguageServerProtocol\FileChangeType; use LanguageServerProtocol\FileEvent; use Psalm\Codebase; +use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\LanguageServer\LanguageServer; /** @@ -24,17 +25,19 @@ class Workspace */ protected $codebase; - /** @var ?int */ - protected $onchange_line_limit; + /** + * @var ProjectAnalyzer + */ + protected $project_analyzer; public function __construct( LanguageServer $server, Codebase $codebase, - ?int $onchange_line_limit + ProjectAnalyzer $project_analyzer ) { $this->server = $server; $this->codebase = $codebase; - $this->onchange_line_limit = $onchange_line_limit; + $this->project_analyzer = $project_analyzer; } /** @@ -62,7 +65,7 @@ public function didChangeWatchedFiles(array $changes): void continue; } - if ($this->onchange_line_limit === 0) { + if ($this->project_analyzer->onchange_line_limit === 0) { continue; }