implements PLN-0136
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview
No changes detected, that affect the code coverage.
* Line Coverage: 63.75% (19407/30442)
* Branch Coverage: 54.17% (7740/14289)
* Lines of Code: 30442
* Cyclomatic Complexity: 12365
#### Quality Gates Summary
Output truncated.
Test / Build skipped: 15, passed: 685
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Some checks are pending
Intrepid/Prometeu/Studio/pipeline/pr-master Build started...
JaCoCo Coverage #### Project Overview
No changes detected, that affect the code coverage.
* Line Coverage: 63.75% (19407/30442)
* Branch Coverage: 54.17% (7740/14289)
* Lines of Code: 30442
* Cyclomatic Complexity: 12365
#### Quality Gates Summary
Output truncated.
Test / Build skipped: 15, passed: 685
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Add PBS quick fixes as frontend-owned repairs transported by the LSP. Duplicate reserved attributes are deleted at the extra span. Interface InitAllowed attributes with arguments become [InitAllowed]. The server matches Diagnostic.code plus range, does not parse messages, and returns an empty list when the capability or the fix is absent. Housekeep DSC-0044 with LSN-0073.
This commit is contained in:
parent
6f45f4f205
commit
ae891262e1
@ -1,4 +1,4 @@
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":57,"PLN":136,"LSN":73,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":58,"PLN":137,"LSN":74,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0065","status":"done","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0067","file":"discussion/lessons/DSC-0065-multi-frontend-avoid-premature-abstractions/LSN-0067-multi-frontend-phase-rejects-premature-infrastructure.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0064","status":"done","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0066","file":"discussion/lessons/DSC-0064-multi-frontend-architectural-tests/LSN-0066-archunit-guards-java-multi-frontend-boundaries.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
{"type":"discussion","id":"DSC-0063","status":"done","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0065","file":"discussion/lessons/DSC-0063-multi-frontend-synthetic-test-frontend/LSN-0065-synthetic-test-frontend-proves-pipeline-neutrality.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
|
||||
@ -20,7 +20,7 @@
|
||||
{"type":"discussion","id":"DSC-0047","status":"open","ticket":"pbs-lsp-semantic-tokens-semantic-classification","title":"PBS LSP Semantic Token Classification","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","semantic-tokens"],"agendas":[{"id":"AGD-0050","file":"AGD-0050-pbs-lsp-semantic-token-classification.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0046","status":"open","ticket":"pbs-lsp-import-assistance","title":"PBS LSP Import Assistance","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","imports","completion"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-pbs-lsp-import-assistance.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0045","status":"open","ticket":"pbs-lsp-formatting","title":"PBS LSP Formatting","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","formatting"],"agendas":[{"id":"AGD-0048","file":"AGD-0048-pbs-lsp-formatting.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0044","status":"open","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[{"id":"AGD-0047","file":"AGD-0047-pbs-lsp-code-actions-and-quick-fixes.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"pbs-lsp-code-actions","title":"PBS LSP Code Actions and Quick Fixes","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","code-actions","quick-fix"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0073","file":"discussion/lessons/DSC-0044-pbs-lsp-code-actions/LSN-0073-pbs-quick-fixes-are-frontend-repairs-transported-by-the-lsp.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0043","status":"done","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-09-22","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0072","file":"discussion/lessons/DSC-0043-pbs-lsp-rename-symbol/LSN-0072-pbs-rename-is-a-validated-physical-workspace-edit.md","status":"done","created_at":"2026-09-22","updated_at":"2026-09-22"}]}
|
||||
{"type":"discussion","id":"DSC-0042","status":"done","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0071","file":"discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
|
||||
{"type":"discussion","id":"DSC-0041","status":"done","ticket":"pbs-lsp-document-symbols-outline","title":"PBS LSP Document Symbols and Outline","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","outline","document-symbols"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0070","file":"discussion/lessons/DSC-0041-pbs-lsp-document-symbols-outline/LSN-0070-pbs-outline-is-an-ast-backed-document-symbol-tree.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
---
|
||||
id: LSN-0073
|
||||
ticket: pbs-lsp-code-actions
|
||||
title: PBS quick fixes are frontend repairs transported by the LSP
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
|
||||
---
|
||||
|
||||
# PBS quick fixes are frontend repairs transported by the LSP
|
||||
|
||||
## Original Problem
|
||||
|
||||
PBS diagnostics already reach the editor, but the server did not advertise `codeActionProvider`. A user could see a duplicate attribute or a bad `InitAllowed` and still had to repair it by hand.
|
||||
|
||||
The tempting shortcut was a code-to-edit table in the LSP. That shortcut does not work. Compiler `Diagnostic` values carry a stable code, phase, template, placeholders, span, and related locations, but the reserved-attribute reports use the short constructor: empty placeholders, and the English sentence is the only fine discriminator. `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` covers Doc without `markdown`, a wrong payload, empty markdown, an unknown argument, `InitAllowed` with arguments, an out-of-range `AssetLowering`, and a bad `Host` shape. A switch on the code cannot pick a safe edit without reading the message. Diagnostic wording is rendering, not identity. Code actions are an optional editorial capability: the frontend owns the meaning, and the LSP transports it.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
PBS quick fix is one `WorkspaceEdit` tied to one diagnostic of the requested physical document. The frontend produces the repair from the syntax tree. The LSP does not invent it.
|
||||
|
||||
Durable locks from `DEC-0057` (AGD-0047: scope A, producer B, residence B, first wave C):
|
||||
|
||||
1. This wave is quick fix only. No organize-imports, source action, refactor, function or struct stub, code lens, or server command.
|
||||
2. The server does not write files. The client applies the edit.
|
||||
3. Each repair names the stable diagnostic code, the primary span of that diagnostic, and exactly one local text edit. The LSP must not keep a code-to-edit table and must not read the diagnostic message to build the edit.
|
||||
4. The repair is computed on `textDocument/codeAction` from the current document analysis. It is not a required field of compiler `Diagnostic`, and it is not published inside `publishDiagnostics`, including `data`.
|
||||
5. `publishDiagnostics` carries the stable compiler code on `Diagnostic.code` so the client can echo it. Source, severity, message, and related information stay as they were. Those presentation questions remain with `AGD-0054`.
|
||||
6. A repair is returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic list returns an empty action list. An `only` filter that does not include quick fix returns an empty action list.
|
||||
7. Each item is a QuickFix `CodeAction`, not a `Command`. The edit touches one file, the requested physical document, and contains one `TextEdit`. There is no lazy resolve.
|
||||
8. A path that is not a compiler-known regular file, including `/virtual/stdlib`, gets no edit.
|
||||
9. If the frontend does not expose the capability, the server does not advertise `codeActionProvider`. A request that arrives anyway returns an empty list, not a protocol error. Absence of a fix is not a refused cursor.
|
||||
10. PBS exposes the capability. Another frontend does not gain it merely by having a language service.
|
||||
11. The first wave is two shapes, not three diagnostic codes.
|
||||
- For every extra reserved attribute the declaration validator already reports as `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE`, delete exactly that occurrence's span. Do not delete the first occurrence, the declaration, or neighboring whitespace.
|
||||
- For an `InitAllowed` attribute with a non-empty argument list on a host signature, the shape the validator reports as `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` when the file is an interface module, replace that attribute span with `[InitAllowed]`. Do not emit that repair for any other attribute that shares the code. `[InitAllowed()]` is not diagnosed today and gets no repair.
|
||||
12. Nothing else gets a quick fix in this wave. In particular: inserting `[Doc(markdown = """""")]` or any other Doc rewrite, inserting an empty or incomplete `[Host]` or `[BuiltinType]`, `W_SEM_IGNORED_VALUE`, unresolved imports, an invalid reserved-attribute target, parse errors, unresolved overloads, and method stubs.
|
||||
|
||||
`AGD-0048` still owns formatting. `AGD-0049` still owns import assistance and organize-imports. `AGD-0054` still owns range, related locations, and diagnostic presentation. The PBS diagnostics spec does not gain a mandatory remediation field.
|
||||
|
||||
Spec 23 §8.3 now states the shared contract: code actions are quick fixes only; the frontend produces each repair from the document syntax; tooling transports it and does not parse the message or switch on codes; the published diagnostic carries the stable code; a repair is returned only for a matching context diagnostic; the edit is one physical file; the server does not write files, return a command, or require resolve; a missing capability is an empty list, not an error; virtual documents get no edit.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
| Layer | What landed |
|
||||
|---|---|
|
||||
| PBS repairs | `PbsQuickFixCollector` walks the same attribute surfaces as `PbsDeclarationSemanticsValidator`, including the interface-module gate. Duplicate reserved attributes become an empty replacement of the extra span. The first `InitAllowed` with arguments on an interface host signature becomes `[InitAllowed]`. |
|
||||
| Source kind | `PbsSemanticReadSurface.sourceKindByFile` records the kind used to parse each file. A missing kind means project rules, so a project `declare host` does not pretend the validator emitted the interface-only `InitAllowed` diagnostic. |
|
||||
| Generic contract | `FrontendLanguageService.codeActionsSupported` defaults false. `codeActions` defaults empty. `FrontendCodeAction` is title, diagnostic code, path, offsets, and `newText`. Empty `newText` is a deletion and is allowed. |
|
||||
| PBS mapping | `PBSFrontendLanguageService` exposes the capability and returns repairs only for the requested regular file. `/virtual/stdlib` fails `Files.isRegularFile` and produces nothing. |
|
||||
| LSP | `BaselineDocumentIssue.code` is copied onto `Diagnostic.code`. `data` stays unset. The bridge drops repairs whose code and range do not match a context diagnostic. `PrometeuTextDocumentService.codeAction` honors the quick-fix `only` filter and never fails the request for “no fix”. |
|
||||
| Protocol | `codeActionProvider` is `CodeActionOptions` with kind `quickfix` and `resolveProvider` false, and only when the frontend reports support. Mapped actions are `CodeAction` values with a `WorkspaceEdit`. They are not `Command`s. |
|
||||
|
||||
Titles are `Remove duplicate reserved attribute` and `Remove InitAllowed arguments`.
|
||||
|
||||
## Examples
|
||||
|
||||
Duplicate `Doc` on a project file. The first copy stays. Each later copy is deleted at its own span:
|
||||
|
||||
```pbs
|
||||
[Doc(markdown = """
|
||||
First.
|
||||
""")]
|
||||
[Doc(markdown = """
|
||||
Second.
|
||||
""")]
|
||||
fn duplicated() -> void { return; }
|
||||
```
|
||||
|
||||
The quick fix removes the second attribute and leaves `First.`. A context diagnostic with the same range but code `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` produces no action. An empty diagnostic list produces no action.
|
||||
|
||||
Malformed or empty Doc is visible and not repairable here:
|
||||
|
||||
```pbs
|
||||
[Doc(text = """
|
||||
Wrong argument.
|
||||
""")]
|
||||
fn malformed() -> void { return; }
|
||||
```
|
||||
|
||||
`InitAllowed` with arguments is repaired only on an interface-module host signature:
|
||||
|
||||
```pbs
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "fill", version = 1)]
|
||||
[InitAllowed()]
|
||||
fn fill() -> void;
|
||||
}
|
||||
```
|
||||
|
||||
`clear` becomes `[InitAllowed]`. `fill` is already valid to the validator, so `[InitAllowed()]` is left alone. The same `InitAllowed(flag = 1)` inside a project file does not get that repair, because the validator does not emit the malformed-attribute diagnostic there.
|
||||
|
||||
The same collector deletes the extra copy of `BuiltinType`, `IntrinsicCall`, `BuiltinConst`, `Host`, and `AssetLowering` when the validator reports `E_SEM_DUPLICATE_RESERVED_ATTRIBUTE` for that surface. A malformed `Host` that merely shares the malformed-attribute code gets no edit.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Do not choose the edit in `prometeu-lsp` by switching on `E_SEM_MALFORMED_RESERVED_ATTRIBUTE` or by parsing the English message. The code is too coarse, and the message is not the identity.
|
||||
|
||||
Do not store the repair on compiler `Diagnostic` or in `Diagnostic.data`. Compilation and quick fix stay different surfaces. The published code exists so the client can echo it back.
|
||||
|
||||
Do not match on `Diagnostic.source` as if it were the long-term identity field. Today the bridge still copies the code into `source` for the existing presentation. The code-action match uses `Diagnostic.code`. `AGD-0054` may change `source` later.
|
||||
|
||||
Do not delete the first duplicate, and do not trim the whitespace around the deleted span. That cleanup is formatting and belongs to `AGD-0048`.
|
||||
|
||||
Do not insert `[Doc(markdown = """""")]`, an empty `[Host]`, or an empty `[BuiltinType]`. Those edits trade one diagnostic for another, or invent a declaration.
|
||||
|
||||
Do not treat `W_SEM_IGNORED_VALUE` as a source edit. Discard exists in lowering, not as PBS syntax.
|
||||
|
||||
Do not offer the `InitAllowed` rewrite for `[InitAllowed()]` or for a project file. The validator does not diagnose those shapes as “arguments not allowed”, so there is no diagnostic to attach.
|
||||
|
||||
Do not return a protocol error when there is no fix. Rename refuses a bad cursor with an error. Quick fix answers “nothing to do” with an empty list.
|
||||
|
||||
Do not advertise `codeActionProvider` for every frontend that has a language service. The default capability is unsupported.
|
||||
|
||||
Do not treat this lesson as closing formatting, import assistance, diagnostic presentation, snapshots, folding, document links, or hierarchy.
|
||||
|
||||
## References
|
||||
|
||||
- Agenda: `AGD-0047` (scope A, producer B, residence B, first wave C)
|
||||
- Decision: `DEC-0057`
|
||||
- Plan: `PLN-0136`
|
||||
- Spec 23 §8.3 — optional code actions; frontend-owned repair; match by code and range; empty list when unsupported
|
||||
- Spec: `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md` — wording is not identity; remediation stays outside the v1 diagnostic minimum
|
||||
- `LSN-0056` — code actions are an optional editorial capability
|
||||
- `LSN-0058` — generic `FrontendLanguageService` for the LSP
|
||||
- `LSN-0067` — no new editorial SPI without a real consumer; PBS implements the existing language service
|
||||
- Still open: `AGD-0048` formatting, `AGD-0049` import assistance, `AGD-0054` diagnostics UX, `AGD-0051` snapshots, `AGD-0056` hierarchy
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0047
|
||||
ticket: pbs-lsp-code-actions
|
||||
title: PBS LSP Code Actions and Quick Fixes
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, code-actions, quick-fix]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS diagnostics are visible, but users do not get actionable quick fixes for common compiler/editor problems, making simple repairs slower than necessary.
|
||||
|
||||
## Context
|
||||
|
||||
The LSP maps diagnostics but does not announce codeActionProvider. Compiler diagnostics already carry codes and ranges, but not every diagnostic has enough structured repair data to generate safe edits.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which diagnostics should receive quick fixes first?
|
||||
- [ ] Should code actions be produced from diagnostic codes only, or from structured compiler repair hints?
|
||||
- [ ] How should imports, reserved attributes, missing methods, and invalid Doc shapes be prioritized?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Diagnostic-code quick fixes
|
||||
|
||||
- **Approach:** Map known diagnostic codes to simple edits in the LSP layer.
|
||||
- **Pro:** Fast for a small set of obvious repairs.
|
||||
- **Con:** Couples fixes to diagnostic text/ranges and can become brittle.
|
||||
- **Maintainability:** Moderate if limited to trivial edits.
|
||||
|
||||
### Option B - Compiler repair hints
|
||||
|
||||
- **Approach:** Extend diagnostics with structured repair metadata and let the LSP map hints to code actions.
|
||||
- **Pro:** Keeps product logic close to compiler knowledge.
|
||||
- **Con:** Requires a new diagnostic/repair contract.
|
||||
- **Maintainability:** Strong for non-trivial fixes and future frontends.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Quick fixes are only useful if safe. Simple code-based fixes can ship first, but imports and generated stubs need stronger compiler context.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B as the target design, with Option A allowed only for a small bootstrap set of unambiguous repairs.
|
||||
|
||||
## Discussion
|
||||
|
||||
This should follow diagnostics UX and import assistance for the first high-value fixes.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Pick the first three diagnostic codes eligible for safe quick fixes.
|
||||
@ -282,6 +282,8 @@ When a frontend exposes workspace symbols, the result MUST be a flat search of n
|
||||
|
||||
When a frontend exposes rename, the result MUST be a validated workspace edit of identifier spans whose locations identify compiler-known physical source files. Identity MUST be the physical declaration locations that definition returns at the cursor. A local or parameter name span MUST still be renameable when definition at that offset is empty. Each rewritten location MUST identify a compiler-known physical source file. A cursor that is not renameable, including builtins, virtual paths, unresolved names, keywords, comments, strings, import aliases and uses of those aliases, error case labels without their own span, and barrel files, MUST produce a protocol error or a null prepareRename result. Tooling MUST NOT return a successful empty workspace edit for a refused cursor or an invalid new name. The new name MUST be a language identifier that is not a keyword and MUST NOT collide in the same binding space, ignoring the sites being rewritten and the overload set being renamed. The server MUST NOT write files and MUST NOT rename file paths. Host prepareRename MUST advertise a prepareProvider when rename is exposed. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability.
|
||||
|
||||
When a frontend exposes code actions, the result MUST be quick fixes only: one workspace edit tied to a diagnostic of the requested physical document. The frontend MUST produce each repair from that document's syntax. Each repair MUST name the stable diagnostic code, the primary span, and exactly one local text edit. Tooling MUST transport that repair. Tooling MUST NOT choose the edit by parsing the diagnostic message or by a diagnostic-code table. A repair MUST NOT be a required field of compiler diagnostics and MUST NOT be published inside diagnostic data. The published diagnostic MUST carry the stable code so a later code-action request can match it. A repair MUST be returned only when the request context contains a diagnostic with the same code and the same primary range. An empty diagnostic context MUST produce an empty list. The edit MUST change only the requested physical source file. The server MUST NOT write files, MUST NOT return a command, and MUST NOT require a resolve round trip. A missing code-action capability MUST NOT advertise code actions and MUST produce an empty list rather than a protocol error. Virtual, untitled, or synthetic documents MUST NOT receive an edit. Code actions MUST NOT include organize-imports, source actions, refactors, or generated stubs unless a future accepted decision says so.
|
||||
|
||||
Compiler diagnostics and editor diagnostics are distinct ownership surfaces. Diagnostics returned by `analyze`, `compile`, and `build` are compiler contract output. Editor diagnostics MAY reuse compiler analysis results, live overlays, caches, or cancellation-aware tooling state, but a frontend MUST NOT be required to provide editor diagnostics in order to compile.
|
||||
|
||||
`FrontendSpec` remains the source of static frontend-owned presentation metadata such as semantic vocabularies, host projections, and visual themes. Producing semantic tokens for a live document is an optional editor-facing capability; the existence of static presentation metadata MUST NOT imply that every frontend can provide live semantic-token results.
|
||||
|
||||
@ -13,9 +13,13 @@ import p.studio.compiler.pbs.semantics.PbsEditorialSignatureHelp;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialSupportService.EditorialDocument;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialRename;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFix;
|
||||
import p.studio.compiler.pbs.semantics.PbsQuickFixCollector;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
@ -397,6 +401,42 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return FrontendRenameResult.applied(List.copyOf(edits.values()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean codeActionsSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendCodeAction> codeActions(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return List.of();
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? null : ast.span().getFileId();
|
||||
final Path path = pathForRegularSource(fileId, fileTable);
|
||||
if (path == null || !path.equals(request.documentPath().toAbsolutePath().normalize())) {
|
||||
return List.of();
|
||||
}
|
||||
final boolean interfaceModule = editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface
|
||||
&& surface.sourceKindByFile() != null
|
||||
&& surface.sourceKindByFile().get(fileId) == SourceKind.SDK_INTERFACE;
|
||||
final ArrayList<FrontendCodeAction> actions = new ArrayList<>();
|
||||
for (final PbsQuickFix repair : PbsQuickFixCollector.collect(ast, interfaceModule)) {
|
||||
actions.add(new FrontendCodeAction(
|
||||
repair.title(),
|
||||
repair.diagnosticCode(),
|
||||
path,
|
||||
repair.startOffset(),
|
||||
repair.endOffset(),
|
||||
repair.newText()));
|
||||
}
|
||||
return List.copyOf(actions);
|
||||
}
|
||||
|
||||
private PbsEditorialRename editorialRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PbsQuickFix(
|
||||
String diagnosticCode,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText,
|
||||
String title) {
|
||||
|
||||
public PbsQuickFix {
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
title = requireText(title, "title");
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.source.Span;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public final class PbsQuickFixCollector {
|
||||
static final String REMOVE_DUPLICATE_TITLE = "Remove duplicate reserved attribute";
|
||||
static final String REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE = "Remove InitAllowed arguments";
|
||||
private static final String INIT_ALLOWED_REPLACEMENT = "[InitAllowed]";
|
||||
|
||||
private PbsQuickFixCollector() {
|
||||
}
|
||||
|
||||
public static List<PbsQuickFix> collect(
|
||||
final PbsAst.File ast,
|
||||
final boolean interfaceModule) {
|
||||
if (ast == null) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<PbsQuickFix> repairs = new ArrayList<>();
|
||||
for (final PbsAst.TopDecl topDecl : ast.topDecls()) {
|
||||
switch (topDecl) {
|
||||
case PbsAst.FunctionDecl functionDecl -> duplicateDoc(functionDecl.attributes(), repairs);
|
||||
case PbsAst.StructDecl structDecl -> {
|
||||
duplicateDoc(structDecl.attributes(), repairs);
|
||||
for (final PbsAst.FunctionDecl method : structDecl.methods()) {
|
||||
duplicateDoc(method.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
case PbsAst.ServiceDecl serviceDecl -> {
|
||||
duplicateDoc(serviceDecl.attributes(), repairs);
|
||||
for (final PbsAst.FunctionDecl method : serviceDecl.methods()) {
|
||||
duplicateDoc(method.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
case PbsAst.ContractDecl contractDecl -> {
|
||||
duplicateDoc(contractDecl.attributes(), repairs);
|
||||
if (interfaceModule) {
|
||||
for (final PbsAst.FunctionSignature signature : contractDecl.signatures()) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.HostDecl hostDecl -> {
|
||||
duplicateDoc(hostDecl.attributes(), repairs);
|
||||
if (interfaceModule) {
|
||||
for (final PbsAst.FunctionSignature signature : hostDecl.signatures()) {
|
||||
collectHostSignature(signature, repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.BuiltinTypeDecl builtinTypeDecl -> {
|
||||
if (interfaceModule) {
|
||||
duplicateDoc(builtinTypeDecl.attributes(), repairs);
|
||||
duplicateNamed(builtinTypeDecl.attributes(), "BuiltinType", repairs);
|
||||
for (final PbsAst.FunctionSignature signature : builtinTypeDecl.signatures()) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
duplicateNamed(signature.attributes(), "IntrinsicCall", repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
case PbsAst.ErrorDecl errorDecl -> duplicateDoc(errorDecl.attributes(), repairs);
|
||||
case PbsAst.EnumDecl enumDecl -> duplicateDoc(enumDecl.attributes(), repairs);
|
||||
case PbsAst.CallbackDecl callbackDecl -> duplicateDoc(callbackDecl.attributes(), repairs);
|
||||
case PbsAst.ConstDecl constDecl -> {
|
||||
duplicateDoc(constDecl.attributes(), repairs);
|
||||
duplicateNamed(constDecl.attributes(), "BuiltinConst", repairs);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
repairs.sort(Comparator
|
||||
.comparingInt(PbsQuickFix::startOffset)
|
||||
.thenComparingInt(PbsQuickFix::endOffset)
|
||||
.thenComparing(PbsQuickFix::diagnosticCode));
|
||||
return List.copyOf(repairs);
|
||||
}
|
||||
|
||||
private static void collectHostSignature(
|
||||
final PbsAst.FunctionSignature signature,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
duplicateDoc(signature.attributes(), repairs);
|
||||
duplicateNamed(signature.attributes(), "Host", repairs);
|
||||
duplicateNamed(signature.attributes(), "InitAllowed", repairs);
|
||||
duplicateNamed(signature.attributes(), "AssetLowering", repairs);
|
||||
final List<PbsAst.Attribute> initAllowed = named(signature.attributes(), "InitAllowed");
|
||||
if (!initAllowed.isEmpty() && !initAllowed.getFirst().arguments().isEmpty()) {
|
||||
add(repairs, initAllowed.getFirst(), PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
|
||||
INIT_ALLOWED_REPLACEMENT, REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void duplicateDoc(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
duplicateNamed(attributes, "Doc", repairs);
|
||||
}
|
||||
|
||||
private static void duplicateNamed(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final String name,
|
||||
final List<PbsQuickFix> repairs) {
|
||||
final List<PbsAst.Attribute> matches = named(attributes, name);
|
||||
for (int index = 1; index < matches.size(); index++) {
|
||||
add(repairs, matches.get(index), PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name(),
|
||||
"", REMOVE_DUPLICATE_TITLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PbsAst.Attribute> named(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final String name) {
|
||||
final ArrayList<PbsAst.Attribute> matches = new ArrayList<>();
|
||||
if (attributes == null) {
|
||||
return matches;
|
||||
}
|
||||
for (final PbsAst.Attribute attribute : attributes) {
|
||||
if (name.equals(attribute.name())) {
|
||||
matches.add(attribute);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static void add(
|
||||
final List<PbsQuickFix> repairs,
|
||||
final PbsAst.Attribute attribute,
|
||||
final String code,
|
||||
final String newText,
|
||||
final String title) {
|
||||
if (attribute == null || attribute.span() == null || attribute.span().isNone()) {
|
||||
return;
|
||||
}
|
||||
final Span span = attribute.span();
|
||||
final long start = span.getStart();
|
||||
final long end = span.getEnd();
|
||||
if (start < 0 || end < start || end > Integer.MAX_VALUE) {
|
||||
return;
|
||||
}
|
||||
repairs.add(new PbsQuickFix(code, (int) start, (int) end, newText, title));
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import p.studio.compiler.messages.BuildingIssueSink;
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
import p.studio.compiler.messages.FrontendPhaseContext;
|
||||
import p.studio.compiler.models.IRBackend;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.PbsFrontendCompiler;
|
||||
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
|
||||
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
|
||||
@ -127,11 +128,15 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
final Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
|
||||
final Map<FileId, SourceKind> sourceKindByFile = new LinkedHashMap<>();
|
||||
for (final var entry : importedSemanticContexts.entrySet()) {
|
||||
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
|
||||
}
|
||||
for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
|
||||
astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast());
|
||||
sourceKindByFile.put(
|
||||
parsedSourceFile.fileId(),
|
||||
parsedSourceFile.sourceKind() == null ? SourceKind.PROJECT : parsedSourceFile.sourceKind());
|
||||
final var importedSemanticContext = importedSemanticContexts.getOrDefault(
|
||||
parsedSourceFile.fileId(),
|
||||
PbsImportedSemanticContext.empty());
|
||||
@ -147,13 +152,15 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
||||
return new PbsSemanticReadSurface(
|
||||
Map.copyOf(astByFile),
|
||||
Map.copyOf(supplementalTopDeclsByFile),
|
||||
Map.copyOf(inlineHintsByFile));
|
||||
Map.copyOf(inlineHintsByFile),
|
||||
Map.copyOf(sourceKindByFile));
|
||||
}
|
||||
|
||||
public record PbsSemanticReadSurface(
|
||||
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
|
||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile,
|
||||
Map<FileId, SourceKind> sourceKindByFile) {
|
||||
}
|
||||
|
||||
private IRBackend mergeCompiledSources(
|
||||
|
||||
@ -0,0 +1,234 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
import p.studio.compiler.pbs.lexer.PbsLexer;
|
||||
import p.studio.compiler.pbs.parser.PbsParser;
|
||||
import p.studio.compiler.source.diagnostics.Diagnostic;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
import p.studio.compiler.source.tables.NameTable;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PbsQuickFixCollectorTest {
|
||||
|
||||
@Test
|
||||
void duplicateDocRepairsMatchValidatorSpansAndKeepTheFirstCopy() {
|
||||
final var parsed = parse("""
|
||||
[Doc(markdown = \"""
|
||||
First.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Second.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Third.
|
||||
\""")]
|
||||
fn duplicated() -> void { return; }
|
||||
|
||||
declare struct Player(x: int) {
|
||||
[Doc(markdown = \"""
|
||||
Kept.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Extra method.
|
||||
\""")]
|
||||
fn update() -> void { return; }
|
||||
}
|
||||
""", SourceKind.PROJECT);
|
||||
|
||||
final var duplicates = diagnostics(parsed, PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name());
|
||||
assertEquals(3, duplicates.size(), parsed.diagnostics().stream().map(Diagnostic::getMessage).toList().toString());
|
||||
assertEquals(3, parsed.repairs().size());
|
||||
assertTrue(parsed.repairs().stream().allMatch(repair ->
|
||||
repair.newText().isEmpty()
|
||||
&& repair.title().equals(PbsQuickFixCollector.REMOVE_DUPLICATE_TITLE)
|
||||
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertTrue(edited.contains("First."));
|
||||
assertTrue(edited.contains("Kept."));
|
||||
assertFalse(edited.contains("Second."));
|
||||
assertFalse(edited.contains("Third."));
|
||||
assertFalse(edited.contains("Extra method."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedAndEmptyDocProduceNoRepair() {
|
||||
final var parsed = parse("""
|
||||
[Doc(text = \"""
|
||||
Wrong argument.
|
||||
\""")]
|
||||
fn malformed() -> void { return; }
|
||||
|
||||
[Doc(markdown = \"""
|
||||
|
||||
\""")]
|
||||
fn empty() -> void { return; }
|
||||
|
||||
[InitAllowed(flag = 1)]
|
||||
fn frame() -> void { return; }
|
||||
""", SourceKind.PROJECT);
|
||||
|
||||
assertTrue(diagnostics(parsed, PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()).size() >= 2);
|
||||
assertTrue(parsed.repairs().isEmpty(), parsed.repairs().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void initAllowedArgumentsAreRewrittenOnlyForInterfaceHostSignatures() {
|
||||
final var parsed = parse("""
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "fill", version = 1)]
|
||||
[InitAllowed()]
|
||||
fn fill() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "blit", version = 1)]
|
||||
[InitAllowed]
|
||||
fn blit() -> void;
|
||||
|
||||
[Host(module = "gfx2d")]
|
||||
fn broken() -> void;
|
||||
|
||||
[Host(module = "gfx2d", name = "copy", version = 1)]
|
||||
[Host(module = "gfx2d", name = "copy", version = 1)]
|
||||
fn copy() -> void;
|
||||
}
|
||||
""", SourceKind.SDK_INTERFACE);
|
||||
|
||||
final var argumentRepairs = parsed.repairs().stream()
|
||||
.filter(repair -> repair.newText().equals("[InitAllowed]"))
|
||||
.toList();
|
||||
assertEquals(1, argumentRepairs.size(), parsed.repairs().toString());
|
||||
assertEquals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), argumentRepairs.getFirst().diagnosticCode());
|
||||
assertEquals(PbsQuickFixCollector.REMOVE_INIT_ALLOWED_ARGUMENTS_TITLE, argumentRepairs.getFirst().title());
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
|
||||
final var duplicateRepairs = parsed.repairs().stream()
|
||||
.filter(repair -> repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name()))
|
||||
.toList();
|
||||
assertEquals(1, duplicateRepairs.size());
|
||||
assertTrue(duplicateRepairs.getFirst().newText().isEmpty());
|
||||
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertTrue(edited.contains("[InitAllowed]\n"));
|
||||
assertFalse(edited.contains("flag = 1"));
|
||||
assertTrue(edited.contains("[InitAllowed()]"));
|
||||
assertEquals(1, count(edited, "[Host(module = \"gfx2d\", name = \"copy\", version = 1)]"));
|
||||
|
||||
final var projectHost = parse("""
|
||||
declare host Gfx {
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[InitAllowed(flag = 1)]
|
||||
fn clear() -> void;
|
||||
}
|
||||
""", SourceKind.PROJECT);
|
||||
assertTrue(projectHost.repairs().stream().noneMatch(repair -> repair.newText().equals("[InitAllowed]")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtinConstTypeIntrinsicAndAssetLoweringDuplicatesAreDeleted() {
|
||||
final var parsed = parse("""
|
||||
[BuiltinType(name = "Color", version = 1)]
|
||||
[BuiltinType(name = "Color", version = 1)]
|
||||
declare builtin type Color(
|
||||
pub raw: int
|
||||
) {
|
||||
[IntrinsicCall(name = "width", version = 1)]
|
||||
[IntrinsicCall(name = "width", version = 1)]
|
||||
fn width() -> int;
|
||||
}
|
||||
[BuiltinConst(target = "Color", name = "white", version = 1)]
|
||||
[BuiltinConst(target = "Color", name = "white", version = 1)]
|
||||
declare const WHITE: Color;
|
||||
declare host LowAssets {
|
||||
[Host(module = "asset", name = "load", version = 1)]
|
||||
[AssetLowering(param = 0)]
|
||||
[AssetLowering(param = 0)]
|
||||
fn load(addressable: int) -> void;
|
||||
}
|
||||
""", SourceKind.SDK_INTERFACE);
|
||||
|
||||
assertEquals(4, parsed.repairs().size(), parsed.repairs().toString());
|
||||
assertTrue(parsed.repairs().stream().allMatch(repair ->
|
||||
repair.newText().isEmpty()
|
||||
&& repair.diagnosticCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
|
||||
assertRepairsMatchDiagnostics(parsed);
|
||||
final String edited = apply(parsed.source(), parsed.repairs());
|
||||
assertEquals(1, count(edited, "[BuiltinType(name = \"Color\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[IntrinsicCall(name = \"width\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[BuiltinConst(target = \"Color\", name = \"white\", version = 1)]"));
|
||||
assertEquals(1, count(edited, "[AssetLowering(param = 0)]"));
|
||||
}
|
||||
|
||||
private static Parsed parse(final String source, final SourceKind sourceKind) {
|
||||
final var diagnostics = DiagnosticSink.empty();
|
||||
final var fileId = new FileId(0);
|
||||
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
|
||||
final var parseMode = sourceKind == SourceKind.SDK_INTERFACE
|
||||
? PbsParser.ParseMode.INTERFACE_MODULE
|
||||
: PbsParser.ParseMode.ORDINARY;
|
||||
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode);
|
||||
new PbsDeclarationSemanticsValidator(new NameTable()).validate(ast, sourceKind, diagnostics);
|
||||
return new Parsed(
|
||||
source,
|
||||
diagnostics,
|
||||
PbsQuickFixCollector.collect(ast, sourceKind == SourceKind.SDK_INTERFACE));
|
||||
}
|
||||
|
||||
private static List<Diagnostic> diagnostics(final Parsed parsed, final String code) {
|
||||
return parsed.diagnostics().stream().filter(diagnostic -> diagnostic.getCode().equals(code)).toList();
|
||||
}
|
||||
|
||||
private static void assertRepairsMatchDiagnostics(final Parsed parsed) {
|
||||
for (final PbsQuickFix repair : parsed.repairs()) {
|
||||
assertTrue(parsed.diagnostics().stream().anyMatch(diagnostic ->
|
||||
diagnostic.getCode().equals(repair.diagnosticCode())
|
||||
&& diagnostic.getSpan().getStart() == repair.startOffset()
|
||||
&& diagnostic.getSpan().getEnd() == repair.endOffset()),
|
||||
repair + " missing from " + parsed.diagnostics());
|
||||
}
|
||||
}
|
||||
|
||||
private static String apply(final String source, final List<PbsQuickFix> repairs) {
|
||||
final var ordered = repairs.stream()
|
||||
.sorted(Comparator.comparingInt(PbsQuickFix::startOffset).reversed())
|
||||
.toList();
|
||||
String current = source;
|
||||
for (final PbsQuickFix repair : ordered) {
|
||||
current = current.substring(0, repair.startOffset()) + repair.newText() + current.substring(repair.endOffset());
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static int count(final String source, final String needle) {
|
||||
int count = 0;
|
||||
int from = 0;
|
||||
while (from <= source.length()) {
|
||||
final int found = source.indexOf(needle, from);
|
||||
if (found < 0) {
|
||||
return count;
|
||||
}
|
||||
count += 1;
|
||||
from = found + needle.length();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private record Parsed(
|
||||
String source,
|
||||
DiagnosticSink diagnostics,
|
||||
List<PbsQuickFix> repairs) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendCodeAction(
|
||||
String title,
|
||||
String diagnosticCode,
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText) {
|
||||
|
||||
public FrontendCodeAction {
|
||||
title = requireText(title, "title");
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
if (startOffset < 0) {
|
||||
throw new IllegalArgumentException("startOffset must not be negative");
|
||||
}
|
||||
if (endOffset < startOffset) {
|
||||
throw new IllegalArgumentException("endOffset must not be before startOffset");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -138,4 +138,18 @@ public interface FrontendLanguageService {
|
||||
final String newName) {
|
||||
return rename(request, offset, newName);
|
||||
}
|
||||
|
||||
default boolean codeActionsSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default List<FrontendCodeAction> codeActions(final FrontendDocumentRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default List<FrontendCodeAction> codeActions(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return codeActions(request);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,38 @@ class FrontendLanguageServiceTest {
|
||||
assertTrue(service.prepareRename(request, 0).isEmpty());
|
||||
assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status());
|
||||
assertTrue(service.rename(request, 0, "renamed").edits().isEmpty());
|
||||
assertFalse(service.codeActionsSupported());
|
||||
assertTrue(service.codeActions(request).isEmpty());
|
||||
assertTrue(service.codeActions(request, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeActionAllowsEmptyReplacementAndRejectsBlankIdentity() {
|
||||
final Path path = Path.of("src/main.pbs").toAbsolutePath();
|
||||
final var action = new FrontendCodeAction(
|
||||
" Remove duplicate reserved attribute ",
|
||||
" E_SEM_DUPLICATE_RESERVED_ATTRIBUTE ",
|
||||
path,
|
||||
4,
|
||||
12,
|
||||
"");
|
||||
assertEquals("Remove duplicate reserved attribute", action.title());
|
||||
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", action.diagnosticCode());
|
||||
assertEquals("", action.newText());
|
||||
assertThrows(IllegalArgumentException.class, () -> new FrontendCodeAction(
|
||||
" ",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
path,
|
||||
0,
|
||||
1,
|
||||
""));
|
||||
assertThrows(NullPointerException.class, () -> new FrontendCodeAction(
|
||||
"Remove duplicate reserved attribute",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
path,
|
||||
0,
|
||||
1,
|
||||
null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineCodeAction(
|
||||
String title,
|
||||
String diagnosticCode,
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineCodeAction {
|
||||
title = requireText(title, "title");
|
||||
diagnosticCode = requireText(diagnosticCode, "diagnosticCode");
|
||||
uri = requireText(uri, "uri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("code action range coordinates must not be negative");
|
||||
}
|
||||
newText = Objects.requireNonNull(newText, "newText");
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
final String value,
|
||||
final String field) {
|
||||
final String candidate = Objects.requireNonNull(value, field).trim();
|
||||
if (candidate.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
public record BaselineCodeActionDiagnostic(
|
||||
String code,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter) {
|
||||
|
||||
public BaselineCodeActionDiagnostic {
|
||||
code = code == null ? "" : code.trim();
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("diagnostic range coordinates must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineCodeActions(List<BaselineCodeAction> actions) {
|
||||
public BaselineCodeActions {
|
||||
actions = actions == null ? List.of() : List.copyOf(actions);
|
||||
}
|
||||
|
||||
public static BaselineCodeActions empty() {
|
||||
return new BaselineCodeActions(List.of());
|
||||
}
|
||||
}
|
||||
@ -9,12 +9,14 @@ public record BaselineDocumentIssue(
|
||||
int endCharacter,
|
||||
BaselineIssueSeverity severity,
|
||||
String source,
|
||||
String message) {
|
||||
String message,
|
||||
String code) {
|
||||
|
||||
public BaselineDocumentIssue {
|
||||
severity = Objects.requireNonNull(severity, "severity");
|
||||
source = requireText(source, "source");
|
||||
message = requireText(message, "message");
|
||||
code = code == null ? "" : code.trim();
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
|
||||
@ -15,6 +15,7 @@ public record BaselineServerDescription(
|
||||
boolean documentSymbolsSupported,
|
||||
boolean workspaceSymbolsSupported,
|
||||
boolean renameSupported,
|
||||
boolean codeActionsSupported,
|
||||
List<String> semanticTokenTypes,
|
||||
List<BaselineSemanticHostProjection> semanticHostProjections,
|
||||
List<BaselineVisualTheme> visualThemes,
|
||||
|
||||
@ -7,6 +7,8 @@ import p.studio.lsp.messages.BaselineHover;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
@ -14,6 +16,8 @@ import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface LanguageServiceBridge {
|
||||
BaselineServerDescription describeServer(LspProjectContext context);
|
||||
|
||||
@ -56,5 +60,11 @@ public interface LanguageServiceBridge {
|
||||
|
||||
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
BaselineCodeActions codeActions(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
List<BaselineCodeActionDiagnostic> diagnostics);
|
||||
|
||||
String onSave(LspProjectContext context, String documentUri);
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.messages.*;
|
||||
import p.studio.compiler.models.*;
|
||||
import p.studio.compiler.services.FrontendCodeAction;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendLanguageService;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
@ -50,7 +52,11 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var frontend = frontendSpec(context);
|
||||
final var presentation = frontend.getSemanticPresentation();
|
||||
final boolean languageServicePresent = frontendProvider(context).languageService().isPresent();
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
final boolean languageServicePresent = languageService.isPresent();
|
||||
final boolean codeActionsSupported = languageService
|
||||
.map(FrontendLanguageService::codeActionsSupported)
|
||||
.orElse(false);
|
||||
return new BaselineServerDescription(
|
||||
"Prometeu Studio LSP",
|
||||
"0.1.0",
|
||||
@ -63,6 +69,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
codeActionsSupported,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -354,6 +361,40 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
.orElseGet(() -> BaselineRename.refused("not renameable"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final List<BaselineCodeActionDiagnostic> diagnostics) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var languageService = frontendProvider(context).languageService();
|
||||
if (languageService.isEmpty() || !languageService.orElseThrow().codeActionsSupported()) {
|
||||
return BaselineCodeActions.empty();
|
||||
}
|
||||
if (diagnostics == null || diagnostics.isEmpty()) {
|
||||
return BaselineCodeActions.empty();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
documentPath,
|
||||
document.text());
|
||||
final List<FrontendCodeAction> repairs = languageService.orElseThrow().codeActions(
|
||||
request,
|
||||
document.editorialContext());
|
||||
return new BaselineCodeActions(mapCodeActions(
|
||||
repairs,
|
||||
documentUri,
|
||||
documentPath,
|
||||
document.text(),
|
||||
diagnostics));
|
||||
})
|
||||
.orElseGet(BaselineCodeActions::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
@ -527,14 +568,16 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
|
||||
final DocumentPosition start = positionMapper.positionOf(startOffset);
|
||||
final DocumentPosition end = positionMapper.positionOf(endOffset);
|
||||
final String code = diagnostic.getCode() == null ? "" : diagnostic.getCode().trim();
|
||||
return new BaselineDocumentIssue(
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
|
||||
diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
|
||||
diagnostic.getMessage());
|
||||
code.isBlank() ? "Prometeu Studio" : code,
|
||||
diagnostic.getMessage(),
|
||||
code);
|
||||
}
|
||||
|
||||
private Path normalizeDocumentPath(final String documentUri) {
|
||||
@ -715,6 +758,45 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
}
|
||||
}
|
||||
|
||||
private List<BaselineCodeAction> mapCodeActions(
|
||||
final List<FrontendCodeAction> repairs,
|
||||
final String documentUri,
|
||||
final Path documentPath,
|
||||
final String documentText,
|
||||
final List<BaselineCodeActionDiagnostic> diagnostics) {
|
||||
if (repairs == null || repairs.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(documentText == null ? "" : documentText);
|
||||
final ArrayList<BaselineCodeAction> mapped = new ArrayList<>();
|
||||
for (final FrontendCodeAction repair : repairs) {
|
||||
if (!repair.path().toAbsolutePath().normalize().equals(documentPath)) {
|
||||
continue;
|
||||
}
|
||||
final DocumentPosition start = mapper.positionOf(repair.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(repair.endOffset());
|
||||
final boolean matched = diagnostics.stream().anyMatch(diagnostic ->
|
||||
repair.diagnosticCode().equals(diagnostic.code())
|
||||
&& diagnostic.startLine() == start.line()
|
||||
&& diagnostic.startCharacter() == start.character()
|
||||
&& diagnostic.endLine() == end.line()
|
||||
&& diagnostic.endCharacter() == end.character());
|
||||
if (!matched) {
|
||||
continue;
|
||||
}
|
||||
mapped.add(new BaselineCodeAction(
|
||||
repair.title(),
|
||||
repair.diagnosticCode(),
|
||||
documentUri,
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
repair.newText()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineRenameEdit> mapRenameEdits(
|
||||
final List<FrontendRenameEdit> edits,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -8,6 +8,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
|
||||
import org.eclipse.lsp4j.services.LanguageClient;
|
||||
import org.eclipse.lsp4j.services.TextDocumentService;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.lsp.services.LanguageServiceBridge;
|
||||
import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper;
|
||||
@ -167,6 +168,22 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(final CodeActionParams params) {
|
||||
if (!quickFixRequested(params.getContext())) {
|
||||
return CompletableFuture.completedFuture(List.of());
|
||||
}
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
final List<Diagnostic> contextDiagnostics = params.getContext() == null
|
||||
|| params.getContext().getDiagnostics() == null
|
||||
? List.of()
|
||||
: params.getContext().getDiagnostics();
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapCodeActions(
|
||||
languageServiceBridge.codeActions(project, uri, text, baselineDiagnostics(contextDiagnostics)),
|
||||
contextDiagnostics));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
|
||||
final DocumentSymbolParams params) {
|
||||
@ -201,6 +218,52 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
languageServiceBridge.semanticTokens(project, uri, text)));
|
||||
}
|
||||
|
||||
private static boolean quickFixRequested(final CodeActionContext context) {
|
||||
if (context == null || context.getOnly() == null || context.getOnly().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (final String kind : context.getOnly()) {
|
||||
if (kind == null || kind.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
if (CodeActionKind.QuickFix.equals(kind) || CodeActionKind.QuickFix.startsWith(kind + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<BaselineCodeActionDiagnostic> baselineDiagnostics(final List<Diagnostic> diagnostics) {
|
||||
final ArrayList<BaselineCodeActionDiagnostic> mapped = new ArrayList<>();
|
||||
for (final Diagnostic diagnostic : diagnostics) {
|
||||
if (diagnostic == null || diagnostic.getRange() == null
|
||||
|| diagnostic.getRange().getStart() == null
|
||||
|| diagnostic.getRange().getEnd() == null) {
|
||||
continue;
|
||||
}
|
||||
mapped.add(new BaselineCodeActionDiagnostic(
|
||||
diagnosticCode(diagnostic),
|
||||
diagnostic.getRange().getStart().getLine(),
|
||||
diagnostic.getRange().getStart().getCharacter(),
|
||||
diagnostic.getRange().getEnd().getLine(),
|
||||
diagnostic.getRange().getEnd().getCharacter()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private static String diagnosticCode(final Diagnostic diagnostic) {
|
||||
if (diagnostic.getCode() == null) {
|
||||
return "";
|
||||
}
|
||||
if (diagnostic.getCode().isLeft()) {
|
||||
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft();
|
||||
}
|
||||
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
|
||||
return diagnostic.getCode().getRight().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void publishAnalysis(
|
||||
final String uri,
|
||||
final String text) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import p.studio.lsp.messages.*;
|
||||
|
||||
@ -46,6 +47,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
renameOptions.setPrepareProvider(true);
|
||||
capabilities.setRenameProvider(renameOptions);
|
||||
}
|
||||
if (description.codeActionsSupported()) {
|
||||
final CodeActionOptions codeActionOptions = new CodeActionOptions();
|
||||
codeActionOptions.setResolveProvider(false);
|
||||
codeActionOptions.setCodeActionKinds(List.of(CodeActionKind.QuickFix));
|
||||
capabilities.setCodeActionProvider(codeActionOptions);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -152,6 +159,31 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Either<Command, CodeAction>> mapCodeActions(
|
||||
final BaselineCodeActions actions,
|
||||
final List<Diagnostic> contextDiagnostics) {
|
||||
if (actions == null || actions.actions().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<Either<Command, CodeAction>> mapped = new ArrayList<>();
|
||||
for (final BaselineCodeAction action : actions.actions()) {
|
||||
final Range range = new Range(
|
||||
new Position(action.startLine(), action.startCharacter()),
|
||||
new Position(action.endLine(), action.endCharacter()));
|
||||
final TextEdit textEdit = new TextEdit(range, action.newText());
|
||||
final WorkspaceEdit edit = new WorkspaceEdit();
|
||||
edit.setChanges(Map.of(action.uri(), List.of(textEdit)));
|
||||
final CodeAction codeAction = new CodeAction();
|
||||
codeAction.setTitle(action.title());
|
||||
codeAction.setKind(CodeActionKind.QuickFix);
|
||||
codeAction.setEdit(edit);
|
||||
codeAction.setDiagnostics(matchingDiagnostics(contextDiagnostics, action.diagnosticCode(), range));
|
||||
mapped.add(Either.forRight(codeAction));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
|
||||
if (symbols == null || symbols.symbols().isEmpty()) {
|
||||
@ -323,6 +355,9 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
diagnostic.setSeverity(mapSeverity(issue.severity()));
|
||||
diagnostic.setSource(issue.source());
|
||||
diagnostic.setMessage(issue.message());
|
||||
if (!issue.code().isBlank()) {
|
||||
diagnostic.setCode(Either.forLeft(issue.code()));
|
||||
}
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
@ -360,6 +395,46 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
}
|
||||
|
||||
|
||||
private List<Diagnostic> matchingDiagnostics(
|
||||
final List<Diagnostic> contextDiagnostics,
|
||||
final String diagnosticCode,
|
||||
final Range range) {
|
||||
if (contextDiagnostics == null || contextDiagnostics.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<Diagnostic> matched = new ArrayList<>();
|
||||
for (final Diagnostic diagnostic : contextDiagnostics) {
|
||||
if (diagnostic == null || diagnostic.getRange() == null) {
|
||||
continue;
|
||||
}
|
||||
if (!diagnosticCode.equals(diagnosticCodeOf(diagnostic)) || !sameRange(diagnostic.getRange(), range)) {
|
||||
continue;
|
||||
}
|
||||
matched.add(diagnostic);
|
||||
}
|
||||
return List.copyOf(matched);
|
||||
}
|
||||
|
||||
private String diagnosticCodeOf(final Diagnostic diagnostic) {
|
||||
if (diagnostic.getCode() == null) {
|
||||
return "";
|
||||
}
|
||||
if (diagnostic.getCode().isLeft()) {
|
||||
return diagnostic.getCode().getLeft() == null ? "" : diagnostic.getCode().getLeft().trim();
|
||||
}
|
||||
if (diagnostic.getCode().isRight() && diagnostic.getCode().getRight() != null) {
|
||||
return diagnostic.getCode().getRight().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean sameRange(final Range left, final Range right) {
|
||||
return left.getStart().getLine() == right.getStart().getLine()
|
||||
&& left.getStart().getCharacter() == right.getStart().getCharacter()
|
||||
&& left.getEnd().getLine() == right.getEnd().getLine()
|
||||
&& left.getEnd().getCharacter() == right.getEnd().getCharacter();
|
||||
}
|
||||
|
||||
private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
|
||||
return switch (severity) {
|
||||
case INFORMATION -> DiagnosticSeverity.Information;
|
||||
|
||||
@ -1,22 +1,27 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.CodeAction;
|
||||
import org.eclipse.lsp4j.Command;
|
||||
import org.eclipse.lsp4j.Hover;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.MessageParams;
|
||||
import org.eclipse.lsp4j.PublishDiagnosticsParams;
|
||||
import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.eclipse.lsp4j.DocumentSymbol;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
import org.eclipse.lsp4j.Range;
|
||||
import org.eclipse.lsp4j.SymbolInformation;
|
||||
import org.eclipse.lsp4j.WorkspaceEdit;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import org.eclipse.lsp4j.PrepareRenameDefaultBehavior;
|
||||
import org.eclipse.lsp4j.PrepareRenameResult;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentSymbols;
|
||||
import p.studio.lsp.messages.BaselineWorkspaceSymbols;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
@ -52,6 +57,8 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
WorkspaceEdit mapRename(BaselineRename rename);
|
||||
|
||||
List<Either<Command, CodeAction>> mapCodeActions(BaselineCodeActions actions, List<Diagnostic> contextDiagnostics);
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -8,11 +8,13 @@ import p.studio.compiler.models.IRBackend;
|
||||
import p.studio.compiler.services.FrontendPhaseService;
|
||||
import p.studio.compiler.services.FrontendProvider;
|
||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||
import p.studio.lsp.messages.BaselineCodeActionDiagnostic;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.LspProjectContext;
|
||||
import p.studio.utilities.logs.LogAggregator;
|
||||
import p.studio.utilities.structures.ReadOnlySet;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@ -68,6 +70,10 @@ class CompilerLanguageServiceBridgeTest {
|
||||
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
|
||||
assertTrue(semanticTokens.legend().isEmpty());
|
||||
assertTrue(semanticTokens.tokens().isEmpty());
|
||||
|
||||
assertTrue(bridge.codeActions(context, documentUri, "fn main() {}", List.of(
|
||||
new BaselineCodeActionDiagnostic("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", 0, 0, 0, 1)))
|
||||
.actions().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -555,6 +561,97 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(stdlibRename.edits().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void quickFixDeletesTheDuplicateDocDiagnosticAndIgnoresOtherCodes() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
.resolve("test-projects")
|
||||
.resolve("main")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
|
||||
final String overlay = """
|
||||
[Doc(markdown = \"""
|
||||
First.
|
||||
\""")]
|
||||
[Doc(markdown = \"""
|
||||
Second.
|
||||
\""")]
|
||||
fn duplicated() -> void { return; }
|
||||
|
||||
fn frame() -> void { return; }
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
final String documentUri = documentPath.toUri().toString();
|
||||
|
||||
final var analysis = bridge.analyzeDocument(context, documentUri, overlay);
|
||||
final var duplicates = analysis.issues().stream()
|
||||
.filter(issue -> issue.code().equals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))
|
||||
.toList();
|
||||
assertEquals(1, duplicates.size(), analysis.issues().toString());
|
||||
final var duplicate = duplicates.getFirst();
|
||||
assertFalse(duplicate.code().isBlank());
|
||||
final var diagnostic = new BaselineCodeActionDiagnostic(
|
||||
duplicate.code(),
|
||||
duplicate.startLine(),
|
||||
duplicate.startCharacter(),
|
||||
duplicate.endLine(),
|
||||
duplicate.endCharacter());
|
||||
|
||||
final var actions = bridge.codeActions(context, documentUri, overlay, List.of(diagnostic));
|
||||
assertEquals(1, actions.actions().size(), actions.toString());
|
||||
final var action = actions.actions().getFirst();
|
||||
assertEquals("Remove duplicate reserved attribute", action.title());
|
||||
assertEquals(documentUri, action.uri());
|
||||
assertEquals("", action.newText());
|
||||
assertEquals(diagnostic.startLine(), action.startLine());
|
||||
assertEquals(diagnostic.startCharacter(), action.startCharacter());
|
||||
assertEquals(diagnostic.endLine(), action.endLine());
|
||||
assertEquals(diagnostic.endCharacter(), action.endCharacter());
|
||||
final String removed = sourceSlice(overlay, action.startLine(), action.startCharacter(), action.endLine(), action.endCharacter());
|
||||
assertTrue(removed.contains("Second."), removed);
|
||||
assertFalse(removed.contains("First."), removed);
|
||||
|
||||
final var wrongCode = new BaselineCodeActionDiagnostic(
|
||||
"E_SEM_MALFORMED_RESERVED_ATTRIBUTE",
|
||||
diagnostic.startLine(),
|
||||
diagnostic.startCharacter(),
|
||||
diagnostic.endLine(),
|
||||
diagnostic.endCharacter());
|
||||
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of(wrongCode)).actions().isEmpty());
|
||||
assertTrue(bridge.codeActions(context, documentUri, overlay, List.of()).actions().isEmpty());
|
||||
|
||||
final String malformed = """
|
||||
[Doc(text = \"""
|
||||
Wrong argument.
|
||||
\""")]
|
||||
fn frame() -> void { return; }
|
||||
""";
|
||||
final var malformedAnalysis = bridge.analyzeDocument(context, documentUri, malformed);
|
||||
final var malformedDiagnostics = malformedAnalysis.issues().stream()
|
||||
.map(issue -> new BaselineCodeActionDiagnostic(
|
||||
issue.code(),
|
||||
issue.startLine(),
|
||||
issue.startCharacter(),
|
||||
issue.endLine(),
|
||||
issue.endCharacter()))
|
||||
.toList();
|
||||
assertTrue(bridge.codeActions(context, documentUri, malformed, malformedDiagnostics).actions().isEmpty());
|
||||
}
|
||||
|
||||
private static String sourceSlice(
|
||||
final String source,
|
||||
final int startLine,
|
||||
final int startCharacter,
|
||||
final int endLine,
|
||||
final int endCharacter) {
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(source);
|
||||
final byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
|
||||
final int start = mapper.byteOffsetOf(startLine, startCharacter);
|
||||
final int end = mapper.byteOffsetOf(endLine, endCharacter);
|
||||
return new String(bytes, start, Math.max(0, end - start), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
@ -667,6 +764,7 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(description.documentSymbolsSupported());
|
||||
assertTrue(description.workspaceSymbolsSupported());
|
||||
assertTrue(description.renameSupported());
|
||||
assertTrue(description.codeActionsSupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
|
||||
@ -104,6 +104,7 @@ class PrometeuLanguageServerTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -223,6 +224,15 @@ class PrometeuLanguageServerTest {
|
||||
return new BaselineSemanticTokens(List.of(), List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
return p.studio.lsp.messages.BaselineCodeActions.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext project, final String documentUri) {
|
||||
return "saved";
|
||||
@ -291,6 +301,13 @@ class PrometeuLanguageServerTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -264,6 +264,15 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return new BaselineSemanticTokens(List.of(), List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
return p.studio.lsp.messages.BaselineCodeActions.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext context, final String documentUri) {
|
||||
return "saved";
|
||||
@ -338,6 +347,13 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return new org.eclipse.lsp4j.WorkspaceEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
return completionList;
|
||||
|
||||
@ -166,6 +166,15 @@ final class PrometeuWorkspaceServiceTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineCodeActions codeActions(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final java.util.List<p.studio.lsp.messages.BaselineCodeActionDiagnostic> diagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext context, final String documentUri) {
|
||||
throw new UnsupportedOperationException();
|
||||
@ -231,6 +240,13 @@ final class PrometeuWorkspaceServiceTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<org.eclipse.lsp4j.jsonrpc.messages.Either<org.eclipse.lsp4j.Command, org.eclipse.lsp4j.CodeAction>> mapCodeActions(
|
||||
final p.studio.lsp.messages.BaselineCodeActions actions,
|
||||
final java.util.List<org.eclipse.lsp4j.Diagnostic> contextDiagnostics) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.CodeActionKind;
|
||||
import org.eclipse.lsp4j.InitializeResult;
|
||||
import org.eclipse.lsp4j.SymbolKind;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.studio.lsp.messages.BaselineCodeAction;
|
||||
import p.studio.lsp.messages.BaselineCodeActions;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDocumentAnalysis;
|
||||
import p.studio.lsp.messages.BaselineDocumentIssue;
|
||||
import p.studio.lsp.messages.BaselineIssueSeverity;
|
||||
import p.studio.lsp.messages.BaselineCompletionItem;
|
||||
import p.studio.lsp.messages.BaselineCompletionItemKind;
|
||||
import p.studio.lsp.messages.BaselineDefinition;
|
||||
@ -28,6 +34,7 @@ import java.util.Map;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class Lsp4jProtocolMessageMapperTest {
|
||||
@ -47,6 +54,7 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -72,6 +80,8 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getWorkspaceSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
|
||||
assertEquals(List.of(CodeActionKind.QuickFix), result.getCapabilities().getCodeActionProvider().getRight().getCodeActionKinds());
|
||||
assertEquals(Boolean.FALSE, result.getCapabilities().getCodeActionProvider().getRight().getResolveProvider());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
@ -248,4 +258,42 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(SymbolKind.EnumMember, mapped.get(1).getKind());
|
||||
assertTrue(mapper.mapWorkspaceSymbols(p.studio.lsp.messages.BaselineWorkspaceSymbols.empty()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void diagnosticsPublishStableCodeAndQuickFixesAreEditsNotCommands() {
|
||||
final var mapper = new Lsp4jProtocolMessageMapper();
|
||||
final var published = mapper.mapDiagnostics("file:///tmp/demo.pbs", new BaselineDocumentAnalysis(List.of(
|
||||
new BaselineDocumentIssue(
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
18,
|
||||
BaselineIssueSeverity.ERROR,
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
"Duplicate Doc attribute on the same declaration surface",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE"))));
|
||||
final var diagnostic = published.getDiagnostics().getFirst();
|
||||
assertEquals("E_SEM_DUPLICATE_RESERVED_ATTRIBUTE", diagnostic.getCode().getLeft());
|
||||
assertNull(diagnostic.getData());
|
||||
|
||||
final var mapped = mapper.mapCodeActions(new BaselineCodeActions(List.of(new BaselineCodeAction(
|
||||
"Remove duplicate reserved attribute",
|
||||
"E_SEM_DUPLICATE_RESERVED_ATTRIBUTE",
|
||||
"file:///tmp/demo.pbs",
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
18,
|
||||
""))), List.of(diagnostic));
|
||||
|
||||
assertEquals(1, mapped.size());
|
||||
assertTrue(mapped.getFirst().isRight());
|
||||
assertFalse(mapped.getFirst().isLeft());
|
||||
final var action = mapped.getFirst().getRight();
|
||||
assertEquals(CodeActionKind.QuickFix, action.getKind());
|
||||
assertNull(action.getCommand());
|
||||
assertEquals("", action.getEdit().getChanges().get("file:///tmp/demo.pbs").getFirst().getNewText());
|
||||
assertEquals(1, action.getDiagnostics().size());
|
||||
assertEquals(diagnostic, action.getDiagnostics().getFirst());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user