implements PLN-0135 #29
@ -1,4 +1,4 @@
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":56,"PLN":135,"LSN":72,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":57,"PLN":136,"LSN":73,"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"}]}
|
||||
@ -21,7 +21,7 @@
|
||||
{"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-0043","status":"open","ticket":"pbs-lsp-rename-symbol","title":"PBS LSP Rename Symbol","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","rename"],"agendas":[{"id":"AGD-0046","file":"AGD-0046-pbs-lsp-rename-symbol.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"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"}]}
|
||||
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"pbs-lsp-find-references","title":"PBS LSP Find References","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","references"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0069","file":"discussion/lessons/DSC-0040-pbs-lsp-find-references/LSN-0069-pbs-find-references-is-the-physical-inverse-of-definition.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]}
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
---
|
||||
id: LSN-0072
|
||||
ticket: pbs-lsp-rename-symbol
|
||||
title: PBS rename is a validated physical workspace edit
|
||||
created: 2026-09-22
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
|
||||
---
|
||||
|
||||
# PBS rename is a validated physical workspace edit
|
||||
|
||||
## Original Problem
|
||||
|
||||
PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), outline (`LSN-0070`), and workspace symbols (`LSN-0071`). Users still could not rename a symbol through the editor. Manual text edits miss uses or rewrite unrelated overloads, comments, strings, and same-name declarations.
|
||||
|
||||
The risk was a file-local rename, a lexeme replace, a new `SymbolId` / `RefIndex`, snapshot cache (`AGD-0051`) as a prerequisite, virtual stdlib documents, or a successful empty `WorkspaceEdit` that pretends the symbol changed.
|
||||
|
||||
## Consolidated Decision
|
||||
|
||||
PBS Rename Symbol is the LSP projection of a **validated `WorkspaceEdit`** over **physical identifier spans** of the **same identity definition already returns**.
|
||||
|
||||
Durable locks from `DEC-0056` (AGD-0046 Q1–Q6 = A):
|
||||
|
||||
1. Identity is the physical declaration-location set from definition. Do not text-search. Do not add `SymbolId` or a persistent `RefIndex`.
|
||||
2. Sites are identifier tokens whose definition intersects that identity, plus identifier-sized declaration name spans. Overloads of the same name in the same owner rewrite together. Members rewrite only that member. Comments, strings, and homonyms of another identity stay.
|
||||
3. Walk regular `fileTable` files of the current `analyze()` surface. Omit `/virtual/stdlib` and other non-regular handles. Every edit location must be a compiler-known physical source file.
|
||||
4. Refuse when physical identity is empty: builtins, virtual stdlib/SDK, unresolved names, keywords, comments, strings, `this`, `Self`. Refuse `mod.barrel`. Refuse `declare error` case labels that have no per-label span.
|
||||
5. Exception: a local or parameter name span is renameable even when definition at that offset is empty. Identity becomes that name span; uses are identifiers whose definition intersects it.
|
||||
6. Refuse when the cursor lexeme is an import alias (`item.alias()` distinct from `item.name()`), including uses of that alias. Do not follow alias definition and rewrite the export. Export rename happens only when the cursor writes the original name. Export rewrite must not change `as Alias` tokens or uses that spell a differing alias.
|
||||
7. `prepareRename` validates the cursor and returns the identifier range, or null. `rename` also validates the new name: PBS identifier, not a keyword, no collision in the same binding space (top-level `fn`/`service`/`global`/`const`, local/param in the same scope, field in the same struct, method in the same owner, case in the same enum), ignoring rewritten sites and the overload set. Failure is an LSP error. Never return a successful empty `WorkspaceEdit`. The server does not write files and does not rename paths.
|
||||
8. Keep the capability on aggregated `FrontendLanguageService`. `prepareRename` defaults empty; `rename` defaults unsupported. PBS maps in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Rename SPI. Advertise `renameProvider` with `prepareProvider`. Compile-only stays unsupported. VS Code stays a thin client.
|
||||
|
||||
Spec 23 §8.3 now also states: rename is a validated workspace edit of identifier spans in compiler-known physical source files; identity is definition; local/param name spans remain renameable; alias, virtual, and error-label cursors refuse; invalid names and collisions are protocol errors, not empty success; the server does not write files; `prepareProvider` is advertised when rename is exposed.
|
||||
|
||||
## Final Implementation
|
||||
|
||||
| Layer | What landed |
|
||||
|---|---|
|
||||
| PBS editorial | `PbsEditorialSupportService.planRename` builds identity from definition or local/param name span, refuses aliases and error labels, walks `IDENTIFIER` tokens whose definition intersects identity and whose lexeme is the original name, then validates identifier/keyword/collision. |
|
||||
| Generic contract | `FrontendLanguageService.prepareRename` defaults empty. `rename` defaults `FrontendRenameResult.unsupported()`. `FrontendRenameEdit` is path + offsets + `newText`. Applied results reject an empty edit list. |
|
||||
| PBS mapping | `PBSFrontendLanguageService` reuses `projectDocuments` and `toFrontendLocation`. Empty physical identity is refuse, even if editorial sites exist on virtual handles. |
|
||||
| LSP | `renameSupported`, `prepareRename`, `rename`. Mapper advertises `RenameOptions.prepareProvider`. Refused rename is `ResponseErrorException`, not an empty `WorkspaceEdit`. Compile-only does not advertise. |
|
||||
| VS Code | Unchanged thin client. |
|
||||
|
||||
Stdlib and SDK modules remain virtual (`/virtual/stdlib/...`). F2 on `Gfx.clear` is refused, matching empty F12 / Shift+F12. Hover is unchanged.
|
||||
|
||||
`let total` at the binding itself still has empty definition. Rename uses the name span as identity so F2 on the binding works.
|
||||
|
||||
## Examples
|
||||
|
||||
Same-file function, including overloads and the declaring identifier. Comment and string stay:
|
||||
|
||||
```pbs
|
||||
fn helper() -> int { return 1; }
|
||||
fn helper(value: int) -> int { return value; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper(2);
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
```
|
||||
|
||||
F2 on `helper` → `aid` rewrites both declarations and both calls.
|
||||
|
||||
Local name span:
|
||||
|
||||
```pbs
|
||||
fn frame() -> void {
|
||||
let total = 1;
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
F2 on `let total` or on the use rewrites both identifier sites.
|
||||
|
||||
Alias is refused. Export rename keeps the local alias:
|
||||
|
||||
```pbs
|
||||
import { Log as Logger } from @sdk:log;
|
||||
Logger.info("a");
|
||||
```
|
||||
|
||||
F2 on `Logger` or `Logger.info` is refused. F2 on a physical `Log` declaration rewrites the original name and `import { Log as Logger }`, and leaves `Logger` / `Logger.info`.
|
||||
|
||||
Builtins and virtual stdlib stay hover-only:
|
||||
|
||||
```pbs
|
||||
let n: int = 1;
|
||||
Gfx.clear(5);
|
||||
```
|
||||
|
||||
Invalid new names (`fn`, `1ab`) and collisions (`helper` → existing `frame`) fail with an LSP error.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Do not implement rename as workspace text search in `lsp-v1` or in the VS Code extension. It will rewrite comments, strings, overloads of another identity, and import aliases.
|
||||
|
||||
Do not follow definition through an import alias. `Logger` currently resolves to `Log`'s span; a blind inverse-of-definition rename would rewrite the export.
|
||||
|
||||
Do not return a successful empty `WorkspaceEdit` for a refused cursor. The editor will treat that as “renamed”.
|
||||
|
||||
Do not skip the local/param name-span exception. F2 on `let total` is the common case, and definition there is empty.
|
||||
|
||||
Do not rewrite `declare error` case labels by using the parent `ErrorDecl` span. Those labels have no per-label span.
|
||||
|
||||
Do not wait for snapshot cache. Rename reuses the request-local walk already used by references.
|
||||
|
||||
Do not treat empty/refused stdlib rename as a missing feature. Virtual paths are not regular files.
|
||||
|
||||
Do not split a Rename SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`).
|
||||
|
||||
Do not treat this lesson as closing snapshots, folding, document links, hierarchy, code actions, formatting, file rename, or local-alias rename.
|
||||
|
||||
## References
|
||||
|
||||
- Agenda: `AGD-0046` (Q1–Q6 = A)
|
||||
- Decision: `DEC-0056`
|
||||
- Plan: `PLN-0135`
|
||||
- Spec 23 §8.3 — optional rename; physical-file-or-empty; alias refuse; error instead of empty success; `prepareProvider`
|
||||
- Spec: `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md` §4.3–4.4
|
||||
- `LSN-0068` — definition identity this feature writes
|
||||
- `LSN-0069` — physical inverse that supplies usage sites
|
||||
- `LSN-0051` — wave 1 deferred rename
|
||||
- `LSN-0058` — generic `FrontendLanguageService` for LSP
|
||||
- `LSN-0047` — protocol stays in `lsp-v1`; compiler owns semantics
|
||||
- `LSN-0067` — no SPI without a real in-repo consumer
|
||||
- Still open: `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy
|
||||
@ -1,62 +0,0 @@
|
||||
---
|
||||
id: AGD-0046
|
||||
ticket: pbs-lsp-rename-symbol
|
||||
title: PBS LSP Rename Symbol
|
||||
status: open
|
||||
created: 2026-07-15
|
||||
resolved:
|
||||
decision:
|
||||
tags: [studio, lsp, vscode, compiler-pbs, editor, rename]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Domain owner: `studio/lsp`
|
||||
|
||||
PBS users cannot safely rename a symbol through the editor, so they must edit text manually and risk missing references or changing unrelated overloads and same-name declarations.
|
||||
|
||||
## Context
|
||||
|
||||
Rename requires the same semantic identity and reference machinery needed by definition/references, plus workspace edits and conflict detection. The current LSP does not announce renameProvider.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which symbols should be renameable in the first wave, and which should be protected such as stdlib, generated, host, or builtin declarations?
|
||||
- [ ] How should rename handle overloads, member methods, imports, and labels with the same text?
|
||||
- [ ] What validation should run before returning a WorkspaceEdit?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Local file rename
|
||||
|
||||
- **Approach:** Rename only references in the current file using local semantic resolution.
|
||||
- **Pro:** Smaller blast radius and easier conflict handling.
|
||||
- **Con:** Incomplete for imported symbols and project-wide APIs.
|
||||
- **Maintainability:** Acceptable only if explicitly presented as limited.
|
||||
|
||||
### Option B - Workspace semantic rename
|
||||
|
||||
- **Approach:** Use a project-wide reference index and return a `WorkspaceEdit` for all usage and declaration sites.
|
||||
- **Pro:** Matches user expectations for editor rename.
|
||||
- **Con:** Requires reliable references, collision checks, and write-safety across files.
|
||||
- **Maintainability:** Strong once references exist.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
Rename is high trust. A partial or text-based rename can corrupt code silently, so scope must be explicit and conservative.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer Option B after semantic references are implemented. If delivered earlier, ship only a clearly limited local rename.
|
||||
|
||||
## Discussion
|
||||
|
||||
Do not implement rename before definition/references are resolved.
|
||||
|
||||
## Resolution
|
||||
|
||||
Ainda em aberto.
|
||||
|
||||
## Next Step
|
||||
|
||||
Decide whether rename waits for workspace references or ships a limited local-only wave.
|
||||
@ -280,6 +280,8 @@ When a frontend exposes document symbols or outline, the result MUST be the name
|
||||
|
||||
When a frontend exposes workspace symbols, the result MUST be a flat search of named declarations whose locations identify compiler-known physical source files of the current analysis. Presence, names, kinds, details, and ranges MUST come from the recovered syntax of those files. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing analysis surface MUST produce an empty list. The workspace-symbol request MUST NOT require an open text document. A declaration without a compiler-known physical file MUST be omitted. Tooling MUST NOT require virtual documents, untitled buffers, or synthetic URIs for this capability. Workspace symbols MUST NOT be a document outline, folding ranges, a persistent symbol index, or a navigation list of virtual locations.
|
||||
|
||||
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.
|
||||
|
||||
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,12 +13,15 @@ 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.pbs.semantics.PbsEditorialRename;
|
||||
import p.studio.compiler.pbs.semantics.PbsEditorialWorkspaceSymbol;
|
||||
import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface;
|
||||
import p.studio.compiler.services.FrontendCompletionCandidate;
|
||||
import p.studio.compiler.services.FrontendDefinitionLocation;
|
||||
import p.studio.compiler.services.FrontendDocumentRequest;
|
||||
import p.studio.compiler.services.FrontendDocumentSymbol;
|
||||
import p.studio.compiler.services.FrontendRenameEdit;
|
||||
import p.studio.compiler.services.FrontendRenameResult;
|
||||
import p.studio.compiler.services.FrontendDocumentation;
|
||||
import p.studio.compiler.services.FrontendEditorialContext;
|
||||
import p.studio.compiler.services.FrontendHover;
|
||||
@ -337,6 +340,101 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService
|
||||
return List.copyOf(symbols);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, null);
|
||||
if (planned == null || !planned.allowed()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (physicalIdentity(planned, fileTable).isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return toFrontendLocation(planned.placeholder(), fileTable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
if (newName == null) {
|
||||
return FrontendRenameResult.refused("invalid identifier");
|
||||
}
|
||||
final PbsEditorialRename planned = editorialRename(request, editorialContext, offset, newName);
|
||||
if (planned == null) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
if (!planned.allowed()) {
|
||||
return FrontendRenameResult.refused(planned.refusal());
|
||||
}
|
||||
if (!(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
if (physicalIdentity(planned, fileTable).isEmpty()) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
final LinkedHashMap<String, FrontendRenameEdit> edits = new LinkedHashMap<>();
|
||||
for (final PbsEditorialLocation site : planned.sites()) {
|
||||
toFrontendLocation(site, fileTable).ifPresent(location ->
|
||||
edits.putIfAbsent(
|
||||
frontendLocationKey(location),
|
||||
new FrontendRenameEdit(
|
||||
location.path(),
|
||||
location.startOffset(),
|
||||
location.endOffset(),
|
||||
newName)));
|
||||
}
|
||||
if (edits.isEmpty()) {
|
||||
return FrontendRenameResult.refused("not renameable");
|
||||
}
|
||||
return FrontendRenameResult.applied(List.copyOf(edits.values()));
|
||||
}
|
||||
|
||||
private PbsEditorialRename editorialRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
if (request == null
|
||||
|| editorialContext == null
|
||||
|| !(editorialContext.syntaxTree() instanceof PbsAst.File ast)
|
||||
|| !(editorialContext.supplementalDeclarations() instanceof ReadOnlyList<?> supplementalTopDecls)
|
||||
|| !(editorialContext.semanticContext() instanceof FESurfaceContext feSurfaceContext)
|
||||
|| !(editorialContext.sourceFiles() instanceof FileTableReader fileTable)) {
|
||||
return null;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
final ReadOnlyList<PbsAst.TopDecl> typedSupplementalTopDecls =
|
||||
(ReadOnlyList<PbsAst.TopDecl>) supplementalTopDecls;
|
||||
final EditorialDocument cursorDocument = new EditorialDocument(
|
||||
request.documentText(),
|
||||
ast,
|
||||
typedSupplementalTopDecls,
|
||||
feSurfaceContext);
|
||||
return editorialSupportService.rename(
|
||||
cursorDocument,
|
||||
offset,
|
||||
newName,
|
||||
projectDocuments(request, editorialContext, cursorDocument, fileTable));
|
||||
}
|
||||
|
||||
private List<FrontendDefinitionLocation> physicalIdentity(
|
||||
final PbsEditorialRename planned,
|
||||
final FileTableReader fileTable) {
|
||||
final ArrayList<FrontendDefinitionLocation> locations = new ArrayList<>();
|
||||
for (final PbsEditorialLocation location : planned.identity()) {
|
||||
toFrontendLocation(location, fileTable).ifPresent(locations::add);
|
||||
}
|
||||
return List.copyOf(locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FrontendSemanticToken> semanticTokens(final FrontendDocumentRequest request) {
|
||||
return semanticTokens(request == null ? "" : request.documentText());
|
||||
|
||||
@ -46,6 +46,32 @@ public final class PbsLexer {
|
||||
return lexer.lexInternal();
|
||||
}
|
||||
|
||||
public static boolean isKeyword(final String text) {
|
||||
return text != null && KEYWORDS.containsKey(text);
|
||||
}
|
||||
|
||||
public static boolean isIdentifierLexeme(final String text) {
|
||||
if (text == null || text.isEmpty() || isKeyword(text)) {
|
||||
return false;
|
||||
}
|
||||
int index = 0;
|
||||
final int first = text.codePointAt(0);
|
||||
if (!(first == '_' || Character.isAlphabetic(first))) {
|
||||
return false;
|
||||
}
|
||||
index += Character.charCount(first);
|
||||
while (index < text.length()) {
|
||||
final int codePoint = text.codePointAt(index);
|
||||
if (!(codePoint == '_'
|
||||
|| Character.isAlphabetic(codePoint)
|
||||
|| Character.isDigit(codePoint))) {
|
||||
return false;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private ReadOnlyList<PbsToken> lexInternal() {
|
||||
while (!isAtEnd() || state != LexerState.DEFAULT) {
|
||||
switch (state) {
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record PbsEditorialRename(
|
||||
String refusal,
|
||||
PbsEditorialLocation placeholder,
|
||||
List<PbsEditorialLocation> identity,
|
||||
List<PbsEditorialLocation> sites) {
|
||||
|
||||
public PbsEditorialRename {
|
||||
refusal = refusal == null ? "" : refusal;
|
||||
identity = identity == null ? List.of() : List.copyOf(identity);
|
||||
sites = sites == null ? List.of() : List.copyOf(sites);
|
||||
}
|
||||
|
||||
public static PbsEditorialRename refused(final String reason) {
|
||||
return new PbsEditorialRename(
|
||||
reason == null || reason.isBlank() ? "not renameable" : reason,
|
||||
null,
|
||||
List.of(),
|
||||
List.of());
|
||||
}
|
||||
|
||||
public static PbsEditorialRename applied(
|
||||
final PbsEditorialLocation placeholder,
|
||||
final List<PbsEditorialLocation> identity,
|
||||
final List<PbsEditorialLocation> sites) {
|
||||
return new PbsEditorialRename(
|
||||
"",
|
||||
Objects.requireNonNull(placeholder, "placeholder"),
|
||||
identity,
|
||||
sites);
|
||||
}
|
||||
|
||||
public boolean allowed() {
|
||||
return refusal.isBlank() && placeholder != null && !identity.isEmpty() && !sites.isEmpty();
|
||||
}
|
||||
}
|
||||
@ -155,6 +155,63 @@ public final class PbsEditorialSupportService {
|
||||
return List.copyOf(usages.values());
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final int offset) {
|
||||
return prepareRename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset);
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final int offset) {
|
||||
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
|
||||
return prepareRename(cursor, offset, List.of(cursor));
|
||||
}
|
||||
|
||||
public Optional<PbsEditorialLocation> prepareRename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final List<EditorialDocument> projectDocuments) {
|
||||
final PbsEditorialRename planned = planRename(cursorDocument, offset, projectDocuments, null);
|
||||
if (!planned.allowed()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(planned.placeholder());
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return rename(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset, newName);
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
|
||||
final FESurfaceContext feSurfaceContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
final EditorialDocument cursor = new EditorialDocument(sourceText, ast, supplementalTopDecls, feSurfaceContext);
|
||||
return rename(cursor, offset, newName, List.of(cursor));
|
||||
}
|
||||
|
||||
public PbsEditorialRename rename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final String newName,
|
||||
final List<EditorialDocument> projectDocuments) {
|
||||
return planRename(cursorDocument, offset, projectDocuments, newName);
|
||||
}
|
||||
|
||||
public record EditorialDocument(
|
||||
String sourceText,
|
||||
PbsAst.File ast,
|
||||
@ -202,6 +259,76 @@ public final class PbsEditorialSupportService {
|
||||
return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter()));
|
||||
}
|
||||
|
||||
private PbsEditorialRename planRename(
|
||||
final EditorialDocument cursorDocument,
|
||||
final int offset,
|
||||
final List<EditorialDocument> projectDocuments,
|
||||
final String newName) {
|
||||
if (cursorDocument == null || cursorDocument.ast() == null) {
|
||||
return PbsEditorialRename.refused("not renameable");
|
||||
}
|
||||
final DocumentContext cursor = buildContext(
|
||||
cursorDocument.sourceText(),
|
||||
cursorDocument.ast(),
|
||||
cursorDocument.supplementalTopDecls(),
|
||||
cursorDocument.feSurfaceContext());
|
||||
final RenameCursor renameCursor = cursor.renameCursorAt(offset);
|
||||
if (renameCursor.refusal() != null) {
|
||||
return PbsEditorialRename.refused(renameCursor.refusal());
|
||||
}
|
||||
if (newName != null) {
|
||||
if (!PbsLexer.isIdentifierLexeme(newName)) {
|
||||
return PbsEditorialRename.refused("invalid identifier");
|
||||
}
|
||||
if (cursor.collides(newName, renameCursor)) {
|
||||
return PbsEditorialRename.refused("name collision");
|
||||
}
|
||||
}
|
||||
final List<EditorialDocument> documents = projectDocuments == null || projectDocuments.isEmpty()
|
||||
? List.of(cursorDocument)
|
||||
: projectDocuments;
|
||||
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
|
||||
sites.putIfAbsent(locationKey(renameCursor.placeholder().span()), renameCursor.placeholder());
|
||||
final Set<String> identityKeys = locationKeys(renameCursor.identity());
|
||||
for (final EditorialDocument document : documents) {
|
||||
if (document == null || document.ast() == null) {
|
||||
continue;
|
||||
}
|
||||
final DocumentContext documentContext = buildContext(
|
||||
document.sourceText(),
|
||||
document.ast(),
|
||||
document.supplementalTopDecls(),
|
||||
document.feSurfaceContext());
|
||||
for (final PbsEditorialLocation site : documentContext.renameSitesMatching(
|
||||
identityKeys,
|
||||
renameCursor.originalName())) {
|
||||
sites.putIfAbsent(locationKey(site.span()), site);
|
||||
}
|
||||
final FileId documentFileId = document.ast().span() == null
|
||||
? FileId.none()
|
||||
: document.ast().span().getFileId();
|
||||
for (final PbsEditorialLocation location : renameCursor.identity()) {
|
||||
if (location == null || location.span() == null || location.span().isNone()) {
|
||||
continue;
|
||||
}
|
||||
if (location.span().length() != renameCursor.originalName().length()) {
|
||||
continue;
|
||||
}
|
||||
if (location.span().getFileId() == null || !location.span().getFileId().equals(documentFileId)) {
|
||||
continue;
|
||||
}
|
||||
sites.putIfAbsent(locationKey(location.span()), location);
|
||||
}
|
||||
}
|
||||
if (sites.isEmpty()) {
|
||||
return PbsEditorialRename.refused("not renameable");
|
||||
}
|
||||
return PbsEditorialRename.applied(
|
||||
renameCursor.placeholder(),
|
||||
renameCursor.identity(),
|
||||
List.copyOf(sites.values()));
|
||||
}
|
||||
|
||||
private DocumentContext buildContext(
|
||||
final String sourceText,
|
||||
final PbsAst.File ast,
|
||||
@ -1499,6 +1626,332 @@ public final class PbsEditorialSupportService {
|
||||
throw new IndexOutOfBoundsException("No significant token at index " + significantIndex);
|
||||
}
|
||||
|
||||
private RenameCursor renameCursorAt(final int offset) {
|
||||
final Integer tokenIndex = identifierTokenIndexAt(offset);
|
||||
if (tokenIndex == null) {
|
||||
return RenameCursor.refused("not an identifier");
|
||||
}
|
||||
final PbsToken token = token(tokenIndex);
|
||||
final String lexeme = token.lexeme();
|
||||
if (isImportAliasLexeme(lexeme)) {
|
||||
return RenameCursor.refused("import alias");
|
||||
}
|
||||
List<PbsEditorialLocation> identity = definitionAt(offset);
|
||||
if (identity.isEmpty()) {
|
||||
identity = localLocations(lexeme, offset);
|
||||
}
|
||||
if (identity.isEmpty()) {
|
||||
identity = parameterLocations(lexeme, offset);
|
||||
}
|
||||
if (isErrorCaseLabel(lexeme, offset, identity)) {
|
||||
return RenameCursor.refused("error case label");
|
||||
}
|
||||
if (identity.isEmpty()) {
|
||||
return RenameCursor.refused("not renameable");
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
|
||||
final Span placeholderSpan = new Span(fileId, token.start(), token.end());
|
||||
if (!hasPhysicalSpan(placeholderSpan)) {
|
||||
return RenameCursor.refused("not renameable");
|
||||
}
|
||||
return new RenameCursor(
|
||||
lexeme,
|
||||
new PbsEditorialLocation(placeholderSpan),
|
||||
identity,
|
||||
null);
|
||||
}
|
||||
|
||||
private List<PbsEditorialLocation> renameSitesMatching(
|
||||
final Set<String> identityKeys,
|
||||
final String originalName) {
|
||||
if (identityKeys == null || identityKeys.isEmpty() || originalName == null || originalName.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
final FileId fileId = ast.span() == null ? FileId.none() : ast.span().getFileId();
|
||||
final LinkedHashMap<String, PbsEditorialLocation> sites = new LinkedHashMap<>();
|
||||
for (final PbsToken token : tokens) {
|
||||
if (token.kind() != PbsTokenKind.IDENTIFIER || !originalName.equals(token.lexeme())) {
|
||||
continue;
|
||||
}
|
||||
if (isImportAliasLexeme(token.lexeme())) {
|
||||
continue;
|
||||
}
|
||||
if (!intersects(definitionAt(token.start()), identityKeys)) {
|
||||
continue;
|
||||
}
|
||||
final Span usageSpan = new Span(fileId, token.start(), token.end());
|
||||
if (!hasPhysicalSpan(usageSpan)) {
|
||||
continue;
|
||||
}
|
||||
sites.putIfAbsent(locationKey(usageSpan), new PbsEditorialLocation(usageSpan));
|
||||
}
|
||||
return List.copyOf(sites.values());
|
||||
}
|
||||
|
||||
private boolean isImportAliasLexeme(final String lexeme) {
|
||||
if (lexeme == null || lexeme.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
for (final var importDecl : ast.imports()) {
|
||||
for (final var item : importDecl.items()) {
|
||||
if (item.alias() == null || item.alias().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (lexeme.equals(item.alias()) && !item.alias().equals(item.name())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isErrorCaseLabel(
|
||||
final String lexeme,
|
||||
final int offset,
|
||||
final List<PbsEditorialLocation> identity) {
|
||||
if (lexeme == null || lexeme.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.ErrorDecl errorDecl)) {
|
||||
continue;
|
||||
}
|
||||
if (lexeme.equals(errorDecl.name()) || !errorDecl.cases().asList().contains(lexeme)) {
|
||||
continue;
|
||||
}
|
||||
if (errorDecl.span() != null && errorDecl.span().contains(offset)) {
|
||||
return true;
|
||||
}
|
||||
final String errorKey = locationKey(errorDecl.span());
|
||||
for (final PbsEditorialLocation location : identity) {
|
||||
if (location != null && location.span() != null && errorKey.equals(locationKey(location.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean collides(
|
||||
final String newName,
|
||||
final RenameCursor renameCursor) {
|
||||
if (newName == null || newName.equals(renameCursor.originalName())) {
|
||||
return false;
|
||||
}
|
||||
if (isLocalOrParameterIdentity(renameCursor)) {
|
||||
return localBindingNames((int) renameCursor.placeholder().span().getStart()).contains(newName);
|
||||
}
|
||||
if (isStructFieldIdentity(renameCursor)) {
|
||||
return structFieldNames(renameCursor).contains(newName);
|
||||
}
|
||||
if (isMethodIdentity(renameCursor)) {
|
||||
return methodNames(renameCursor).contains(newName);
|
||||
}
|
||||
if (isEnumCaseIdentity(renameCursor)) {
|
||||
return enumCaseNames(renameCursor).contains(newName);
|
||||
}
|
||||
return topLevelBindingNames().contains(newName);
|
||||
}
|
||||
|
||||
private boolean isLocalOrParameterIdentity(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl) || functionDecl.body() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final var parameter : functionDecl.parameters()) {
|
||||
if (keys.contains(locationKey(parameter.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (containsLetNameSpan(functionDecl.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var method : structDecl.methods()) {
|
||||
if (method.body() == null) {
|
||||
continue;
|
||||
}
|
||||
for (final var parameter : method.parameters()) {
|
||||
if (keys.contains(locationKey(parameter.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (containsLetNameSpan(method.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsLetNameSpan(
|
||||
final PbsAst.Block block,
|
||||
final Set<String> keys) {
|
||||
if (block == null) {
|
||||
return false;
|
||||
}
|
||||
for (final var statement : block.statements()) {
|
||||
if (statement instanceof PbsAst.LetStatement letStatement
|
||||
&& letStatement.nameSpan() != null
|
||||
&& keys.contains(locationKey(letStatement.nameSpan()))) {
|
||||
return true;
|
||||
}
|
||||
if (statement instanceof PbsAst.IfStatement ifStatement) {
|
||||
if (containsLetNameSpan(ifStatement.thenBlock(), keys)
|
||||
|| containsLetNameSpan(ifStatement.elseBlock(), keys)) {
|
||||
return true;
|
||||
}
|
||||
} else if (statement instanceof PbsAst.ForStatement forStatement) {
|
||||
if (containsLetNameSpan(forStatement.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
} else if (statement instanceof PbsAst.WhileStatement whileStatement
|
||||
&& containsLetNameSpan(whileStatement.body(), keys)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> localBindingNames(final int offset) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
final CallableFrame frame = callableFrameAt(offset);
|
||||
if (frame == null) {
|
||||
return Set.of();
|
||||
}
|
||||
names.addAll(frame.parameterNames());
|
||||
collectLetNames(frame.body(), names);
|
||||
return names;
|
||||
}
|
||||
|
||||
private void collectLetNames(
|
||||
final PbsAst.Block block,
|
||||
final Set<String> names) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
for (final var statement : block.statements()) {
|
||||
if (statement instanceof PbsAst.LetStatement letStatement) {
|
||||
names.add(letStatement.name());
|
||||
} else if (statement instanceof PbsAst.IfStatement ifStatement) {
|
||||
collectLetNames(ifStatement.thenBlock(), names);
|
||||
collectLetNames(ifStatement.elseBlock(), names);
|
||||
} else if (statement instanceof PbsAst.ForStatement forStatement) {
|
||||
names.add(forStatement.iteratorName());
|
||||
collectLetNames(forStatement.body(), names);
|
||||
} else if (statement instanceof PbsAst.WhileStatement whileStatement) {
|
||||
collectLetNames(whileStatement.body(), names);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStructFieldIdentity(final RenameCursor renameCursor) {
|
||||
return !structFieldNames(renameCursor).isEmpty()
|
||||
&& structFieldIdentityMatch(renameCursor);
|
||||
}
|
||||
|
||||
private boolean structFieldIdentityMatch(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var field : structDecl.fields()) {
|
||||
if (keys.contains(locationKey(field.span()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<String> structFieldNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var field : structDecl.fields()) {
|
||||
if (keys.contains(locationKey(field.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : structDecl.fields()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private boolean isMethodIdentity(final RenameCursor renameCursor) {
|
||||
return !methodNames(renameCursor).isEmpty();
|
||||
}
|
||||
|
||||
private Set<String> methodNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.StructDecl structDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var method : structDecl.methods()) {
|
||||
if (keys.contains(locationKey(method.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : structDecl.methods()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private boolean isEnumCaseIdentity(final RenameCursor renameCursor) {
|
||||
return !enumCaseNames(renameCursor).isEmpty();
|
||||
}
|
||||
|
||||
private Set<String> enumCaseNames(final RenameCursor renameCursor) {
|
||||
final Set<String> keys = locationKeys(renameCursor.identity());
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (!(topDecl instanceof PbsAst.EnumDecl enumDecl)) {
|
||||
continue;
|
||||
}
|
||||
for (final var enumCase : enumDecl.cases()) {
|
||||
if (keys.contains(locationKey(enumCase.span()))) {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var sibling : enumDecl.cases()) {
|
||||
names.add(sibling.name());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private Set<String> topLevelBindingNames() {
|
||||
final LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
|
||||
names.add(functionDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
|
||||
names.add(serviceDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
|
||||
names.add(globalDecl.name());
|
||||
} else if (topDecl instanceof PbsAst.ConstDecl constDecl) {
|
||||
names.add(constDecl.name());
|
||||
}
|
||||
}
|
||||
names.addAll(importedSymbols.keySet());
|
||||
return names;
|
||||
}
|
||||
|
||||
private PbsToken token(final int index) {
|
||||
return tokens.get(index);
|
||||
}
|
||||
@ -1707,6 +2160,17 @@ public final class PbsEditorialSupportService {
|
||||
String origin) {
|
||||
}
|
||||
|
||||
private record RenameCursor(
|
||||
String originalName,
|
||||
PbsEditorialLocation placeholder,
|
||||
List<PbsEditorialLocation> identity,
|
||||
String refusal) {
|
||||
|
||||
private static RenameCursor refused(final String reason) {
|
||||
return new RenameCursor("", null, List.of(), reason);
|
||||
}
|
||||
}
|
||||
|
||||
private record CallableFrame(
|
||||
ReadOnlyList<PbsAst.Parameter> parameters,
|
||||
PbsAst.Block body,
|
||||
|
||||
@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets;
|
||||
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 PbsLexerTest {
|
||||
@ -34,6 +35,13 @@ class PbsLexerTest {
|
||||
.kind());
|
||||
assertEquals(PbsTokenKind.EOF, tokens.getLast().kind());
|
||||
assertTrue(diagnostics.isEmpty(), "Lexer should not report diagnostics for valid input");
|
||||
assertTrue(PbsLexer.isKeyword("fn"));
|
||||
assertTrue(PbsLexer.isKeyword("step"));
|
||||
assertTrue(PbsLexer.isIdentifierLexeme("helper"));
|
||||
assertTrue(PbsLexer.isIdentifierLexeme("_value"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme("fn"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme("1ab"));
|
||||
assertFalse(PbsLexer.isIdentifierLexeme(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -606,6 +606,132 @@ final class PbsEditorialSupportServiceTest {
|
||||
assertEquals(List.of("r", "pack"), names(symbols.get(1).children()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameRewritesPhysicalFunctionSitesAndSkipsCommentsStringsAndCollisions() {
|
||||
final var source = """
|
||||
fn helper() -> int { return 1; }
|
||||
|
||||
fn helper(value: int) -> int { return value; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper(2);
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
""";
|
||||
final var ast = parseOrdinary(source);
|
||||
final var renamed = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "aid");
|
||||
assertTrue(renamed.allowed(), renamed.refusal());
|
||||
assertEquals(4, renamed.sites().size(), renamed.sites().toString());
|
||||
assertTrue(service.prepareRename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();")).isPresent());
|
||||
|
||||
final var keyword = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "fn");
|
||||
assertFalse(keyword.allowed());
|
||||
assertEquals("invalid identifier", keyword.refusal());
|
||||
|
||||
final var collision = service.rename(source, ast, ReadOnlyList.empty(), source.indexOf("helper();"), "frame");
|
||||
assertFalse(collision.allowed());
|
||||
assertEquals("name collision", collision.refusal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameAllowsLocalNameSpanAndRefusesAliasBuiltinAndErrorLabel() {
|
||||
final var source = """
|
||||
import { Log as Logger } from @sdk:log;
|
||||
|
||||
declare error Boom {
|
||||
Missing;
|
||||
}
|
||||
|
||||
fn frame() -> void {
|
||||
let total = 1;
|
||||
Logger.info("x");
|
||||
let n: int = total;
|
||||
n;
|
||||
}
|
||||
""";
|
||||
final var ast = parseOrdinary(source);
|
||||
final var supplemental = supplementalTopDecls("""
|
||||
declare service Log {
|
||||
fn info(message: str) -> void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
final var local = service.rename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("total ="),
|
||||
"sum");
|
||||
assertTrue(local.allowed(), local.refusal());
|
||||
assertEquals(2, local.sites().size(), local.sites().toString());
|
||||
|
||||
final var alias = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("Logger.info"));
|
||||
assertTrue(alias.isEmpty());
|
||||
|
||||
final var builtin = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("int = total"));
|
||||
assertTrue(builtin.isEmpty());
|
||||
|
||||
final var errorLabel = service.prepareRename(
|
||||
source,
|
||||
ast,
|
||||
supplemental,
|
||||
source.indexOf("Missing"));
|
||||
assertTrue(errorLabel.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameOfExportDoesNotRewriteDifferingAlias() {
|
||||
final var exporter = """
|
||||
fn helper() -> int { return 1; }
|
||||
""";
|
||||
final var importer = """
|
||||
import { helper as aid } from @sdk:log;
|
||||
|
||||
fn frame() -> void {
|
||||
aid();
|
||||
}
|
||||
""";
|
||||
final var exporterAst = parse(exporter, new FileId(1), PbsParser.ParseMode.ORDINARY);
|
||||
final var importerAst = parseOrdinary(importer);
|
||||
final var exporterDoc = new PbsEditorialSupportService.EditorialDocument(
|
||||
exporter,
|
||||
exporterAst,
|
||||
ReadOnlyList.empty(),
|
||||
p.studio.compiler.messages.FESurfaceContext.empty());
|
||||
final var importerDoc = new PbsEditorialSupportService.EditorialDocument(
|
||||
importer,
|
||||
importerAst,
|
||||
exporterAst.topDecls(),
|
||||
p.studio.compiler.messages.FESurfaceContext.empty());
|
||||
final var renamed = service.rename(
|
||||
exporterDoc,
|
||||
exporter.indexOf("helper"),
|
||||
"renamed",
|
||||
List.of(exporterDoc, importerDoc));
|
||||
assertTrue(renamed.allowed(), renamed.refusal());
|
||||
assertEquals(2, renamed.sites().size(), renamed.sites().toString());
|
||||
assertTrue(renamed.sites().stream().anyMatch(site ->
|
||||
site.span().getFileId().equals(exporterAst.span().getFileId())));
|
||||
assertTrue(renamed.sites().stream().anyMatch(site ->
|
||||
site.span().getFileId().equals(importerAst.span().getFileId())));
|
||||
final int aliasUse = importer.indexOf("aid();");
|
||||
assertTrue(renamed.sites().stream().noneMatch(site ->
|
||||
site.span().getFileId().equals(importerAst.span().getFileId())
|
||||
&& site.span().getStart() == aliasUse));
|
||||
}
|
||||
|
||||
private PbsEditorialResolvedSymbol requireHover(
|
||||
final String source,
|
||||
final PbsAst.File ast,
|
||||
|
||||
@ -110,4 +110,32 @@ public interface FrontendLanguageService {
|
||||
final FrontendEditorialContext editorialContext) {
|
||||
return workspaceSymbols(request);
|
||||
}
|
||||
|
||||
default Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default Optional<FrontendDefinitionLocation> prepareRename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset) {
|
||||
return prepareRename(request, offset);
|
||||
}
|
||||
|
||||
default FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return FrontendRenameResult.unsupported();
|
||||
}
|
||||
|
||||
default FrontendRenameResult rename(
|
||||
final FrontendDocumentRequest request,
|
||||
final FrontendEditorialContext editorialContext,
|
||||
final int offset,
|
||||
final String newName) {
|
||||
return rename(request, offset, newName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendRenameEdit(
|
||||
Path path,
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
String newText) {
|
||||
|
||||
public FrontendRenameEdit {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package p.studio.compiler.services;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record FrontendRenameResult(
|
||||
Status status,
|
||||
String message,
|
||||
List<FrontendRenameEdit> edits) {
|
||||
|
||||
public enum Status {
|
||||
UNSUPPORTED,
|
||||
REFUSED,
|
||||
APPLIED
|
||||
}
|
||||
|
||||
public FrontendRenameResult {
|
||||
status = Objects.requireNonNull(status, "status");
|
||||
message = message == null ? "" : message.trim();
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
if (status == Status.APPLIED && edits.isEmpty()) {
|
||||
throw new IllegalArgumentException("applied rename must include at least one edit");
|
||||
}
|
||||
if (status != Status.APPLIED && !edits.isEmpty()) {
|
||||
throw new IllegalArgumentException("refused or unsupported rename must not include edits");
|
||||
}
|
||||
}
|
||||
|
||||
public static FrontendRenameResult unsupported() {
|
||||
return new FrontendRenameResult(Status.UNSUPPORTED, "unsupported", List.of());
|
||||
}
|
||||
|
||||
public static FrontendRenameResult refused(final String message) {
|
||||
final String reason = message == null || message.isBlank() ? "not renameable" : message.trim();
|
||||
return new FrontendRenameResult(Status.REFUSED, reason, List.of());
|
||||
}
|
||||
|
||||
public static FrontendRenameResult applied(final List<FrontendRenameEdit> edits) {
|
||||
return new FrontendRenameResult(Status.APPLIED, "", edits);
|
||||
}
|
||||
|
||||
public boolean applied() {
|
||||
return status == Status.APPLIED;
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,9 @@ class FrontendLanguageServiceTest {
|
||||
assertTrue(service.documentSymbols(request).isEmpty());
|
||||
assertTrue(service.workspaceSymbols(new FrontendWorkspaceRequest(Path.of("."), "frame")).isEmpty());
|
||||
assertTrue(service.prepareEditorialContext(null).isEmpty());
|
||||
assertTrue(service.prepareRename(request, 0).isEmpty());
|
||||
assertEquals(FrontendRenameResult.Status.UNSUPPORTED, service.rename(request, 0, "renamed").status());
|
||||
assertTrue(service.rename(request, 0, "renamed").edits().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record BaselinePrepareRename(Optional<BaselineDefinitionLocation> placeholder) {
|
||||
|
||||
public BaselinePrepareRename {
|
||||
placeholder = placeholder == null ? Optional.empty() : placeholder;
|
||||
}
|
||||
|
||||
public static BaselinePrepareRename none() {
|
||||
return new BaselinePrepareRename(Optional.empty());
|
||||
}
|
||||
|
||||
public static BaselinePrepareRename of(final BaselineDefinitionLocation location) {
|
||||
return new BaselinePrepareRename(Optional.of(location));
|
||||
}
|
||||
|
||||
public boolean renameable() {
|
||||
return placeholder.isPresent();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record BaselineRename(
|
||||
boolean applied,
|
||||
String message,
|
||||
List<BaselineRenameEdit> edits) {
|
||||
|
||||
public BaselineRename {
|
||||
message = message == null ? "" : message;
|
||||
edits = edits == null ? List.of() : List.copyOf(edits);
|
||||
}
|
||||
|
||||
public static BaselineRename unsupported() {
|
||||
return new BaselineRename(false, "unsupported", List.of());
|
||||
}
|
||||
|
||||
public static BaselineRename refused(final String message) {
|
||||
return new BaselineRename(false, message == null || message.isBlank() ? "not renameable" : message, List.of());
|
||||
}
|
||||
|
||||
public static BaselineRename applied(final List<BaselineRenameEdit> edits) {
|
||||
return new BaselineRename(true, "", edits);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package p.studio.lsp.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record BaselineRenameEdit(
|
||||
String uri,
|
||||
int startLine,
|
||||
int startCharacter,
|
||||
int endLine,
|
||||
int endCharacter,
|
||||
String newText) {
|
||||
|
||||
public BaselineRenameEdit {
|
||||
uri = requireText(uri, "uri");
|
||||
if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) {
|
||||
throw new IllegalArgumentException("rename 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;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ public record BaselineServerDescription(
|
||||
boolean referencesSupported,
|
||||
boolean documentSymbolsSupported,
|
||||
boolean workspaceSymbolsSupported,
|
||||
boolean renameSupported,
|
||||
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.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineSemanticTokens;
|
||||
import p.studio.lsp.messages.BaselineSignatureHelp;
|
||||
import p.studio.lsp.messages.BaselineServerDescription;
|
||||
@ -35,6 +37,21 @@ public interface LanguageServiceBridge {
|
||||
|
||||
BaselineWorkspaceSymbols workspaceSymbols(LspProjectContext context, String query);
|
||||
|
||||
BaselinePrepareRename prepareRename(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
int line,
|
||||
int character);
|
||||
|
||||
BaselineRename rename(
|
||||
LspProjectContext context,
|
||||
String documentUri,
|
||||
String text,
|
||||
int line,
|
||||
int character,
|
||||
String newName);
|
||||
|
||||
BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character);
|
||||
|
||||
BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text);
|
||||
|
||||
@ -14,6 +14,8 @@ import p.studio.compiler.services.FrontendProvider;
|
||||
import p.studio.compiler.services.FrontendSignature;
|
||||
import p.studio.compiler.services.FrontendSignatureHelp;
|
||||
import p.studio.compiler.services.FrontendSymbolKind;
|
||||
import p.studio.compiler.services.FrontendRenameEdit;
|
||||
import p.studio.compiler.services.FrontendRenameResult;
|
||||
import p.studio.compiler.services.FrontendWorkspaceRequest;
|
||||
import p.studio.compiler.services.FrontendWorkspaceSymbol;
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
@ -60,6 +62,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
languageServicePresent,
|
||||
presentation.semanticKeys(),
|
||||
presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(),
|
||||
presentation.themes().stream().map(this::mapVisualTheme).toList(),
|
||||
@ -272,6 +275,85 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
.orElseGet(BaselineWorkspaceSymbols::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var provider = frontendProvider(context);
|
||||
final var languageService = provider.languageService();
|
||||
if (languageService.isEmpty()) {
|
||||
return BaselinePrepareRename.none();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.flatMap(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
document.text());
|
||||
return languageService.orElseThrow().prepareRename(
|
||||
request,
|
||||
document.editorialContext(),
|
||||
offset);
|
||||
})
|
||||
.map(location -> {
|
||||
final Path documentPath = normalizeDocumentPath(documentUri);
|
||||
final List<BaselineDefinitionLocation> mapped = mapDefinitionLocations(
|
||||
List.of(location),
|
||||
documentPath,
|
||||
text == null ? "" : text);
|
||||
if (mapped.isEmpty()) {
|
||||
return BaselinePrepareRename.none();
|
||||
}
|
||||
return BaselinePrepareRename.of(mapped.getFirst());
|
||||
})
|
||||
.orElseGet(BaselinePrepareRename::none);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
Objects.requireNonNull(context, "context");
|
||||
final var provider = frontendProvider(context);
|
||||
final var languageService = provider.languageService();
|
||||
if (languageService.isEmpty()) {
|
||||
return BaselineRename.unsupported();
|
||||
}
|
||||
return editorialDocument(context, documentUri, text)
|
||||
.map(document -> {
|
||||
final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character);
|
||||
final FrontendDocumentRequest request = new FrontendDocumentRequest(
|
||||
context.projectRoot(),
|
||||
normalizeDocumentPath(documentUri),
|
||||
document.text());
|
||||
final FrontendRenameResult result = languageService.orElseThrow().rename(
|
||||
request,
|
||||
document.editorialContext(),
|
||||
offset,
|
||||
newName);
|
||||
if (!result.applied()) {
|
||||
if (result.status() == FrontendRenameResult.Status.UNSUPPORTED) {
|
||||
return BaselineRename.unsupported();
|
||||
}
|
||||
return BaselineRename.refused(result.message());
|
||||
}
|
||||
return BaselineRename.applied(mapRenameEdits(
|
||||
result.edits(),
|
||||
request.documentPath(),
|
||||
document.text()));
|
||||
})
|
||||
.orElseGet(() -> BaselineRename.refused("not renameable"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
@ -633,6 +715,33 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
}
|
||||
}
|
||||
|
||||
private List<BaselineRenameEdit> mapRenameEdits(
|
||||
final List<FrontendRenameEdit> edits,
|
||||
final Path currentDocumentPath,
|
||||
final String currentDocumentText) {
|
||||
if (edits == null || edits.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final ArrayList<BaselineRenameEdit> mapped = new ArrayList<>();
|
||||
for (final FrontendRenameEdit edit : edits) {
|
||||
final String targetText = textForDefinitionTarget(edit.path(), currentDocumentPath, currentDocumentText);
|
||||
if (targetText == null) {
|
||||
continue;
|
||||
}
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(targetText);
|
||||
final DocumentPosition start = mapper.positionOf(edit.startOffset());
|
||||
final DocumentPosition end = mapper.positionOf(edit.endOffset());
|
||||
mapped.add(new BaselineRenameEdit(
|
||||
edit.path().toUri().toString(),
|
||||
start.line(),
|
||||
start.character(),
|
||||
end.line(),
|
||||
end.character(),
|
||||
edit.newText()));
|
||||
}
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
private List<BaselineDefinitionLocation> mapDefinitionLocations(
|
||||
final List<FrontendDefinitionLocation> locations,
|
||||
final Path currentDocumentPath,
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
package p.studio.lsp.services.protocol;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
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.LspProjectContext;
|
||||
@ -128,6 +132,41 @@ public final class PrometeuTextDocumentService implements TextDocumentService {
|
||||
includeDeclaration)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior>> prepareRename(
|
||||
final PrepareRenameParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapPrepareRename(
|
||||
languageServiceBridge.prepareRename(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<WorkspaceEdit> rename(final RenameParams params) {
|
||||
final String uri = params.getTextDocument().getUri();
|
||||
final String text = documentTextByUri.get(uri);
|
||||
final var result = languageServiceBridge.rename(
|
||||
project,
|
||||
uri,
|
||||
text,
|
||||
params.getPosition().getLine(),
|
||||
params.getPosition().getCharacter(),
|
||||
params.getNewName());
|
||||
if (!result.applied()) {
|
||||
final ResponseError error = new ResponseError(
|
||||
ResponseErrorCode.InvalidRequest,
|
||||
result.message() == null || result.message().isBlank() ? "not renameable" : result.message(),
|
||||
null);
|
||||
return CompletableFuture.failedFuture(new ResponseErrorException(error));
|
||||
}
|
||||
return CompletableFuture.completedFuture(protocolMessageMapper.mapRename(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(
|
||||
final DocumentSymbolParams params) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.lsp.services.protocol.mapping;
|
||||
|
||||
import org.eclipse.lsp4j.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.messages.Either3;
|
||||
import p.studio.lsp.messages.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -40,6 +41,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
capabilities.setReferencesProvider(description.referencesSupported());
|
||||
capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported());
|
||||
capabilities.setWorkspaceSymbolProvider(description.workspaceSymbolsSupported());
|
||||
if (description.renameSupported()) {
|
||||
final RenameOptions renameOptions = new RenameOptions();
|
||||
renameOptions.setPrepareProvider(true);
|
||||
capabilities.setRenameProvider(renameOptions);
|
||||
}
|
||||
final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions();
|
||||
semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of()));
|
||||
semanticTokens.setFull(true);
|
||||
@ -115,6 +121,37 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
|
||||
return List.copyOf(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final BaselinePrepareRename prepareRename) {
|
||||
if (prepareRename == null || prepareRename.placeholder().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final BaselineDefinitionLocation location = prepareRename.placeholder().orElseThrow();
|
||||
return Either3.forFirst(new Range(
|
||||
new Position(location.startLine(), location.startCharacter()),
|
||||
new Position(location.endLine(), location.endCharacter())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkspaceEdit mapRename(final BaselineRename rename) {
|
||||
final WorkspaceEdit edit = new WorkspaceEdit();
|
||||
if (rename == null || !rename.applied() || rename.edits().isEmpty()) {
|
||||
edit.setChanges(Map.of());
|
||||
return edit;
|
||||
}
|
||||
final LinkedHashMap<String, List<TextEdit>> changes = new LinkedHashMap<>();
|
||||
for (final BaselineRenameEdit renameEdit : rename.edits()) {
|
||||
changes.computeIfAbsent(renameEdit.uri(), ignored -> new ArrayList<>()).add(new TextEdit(
|
||||
new Range(
|
||||
new Position(renameEdit.startLine(), renameEdit.startCharacter()),
|
||||
new Position(renameEdit.endLine(), renameEdit.endCharacter())),
|
||||
renameEdit.newText()));
|
||||
}
|
||||
edit.setChanges(changes);
|
||||
return edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SymbolInformation> mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) {
|
||||
if (symbols == null || symbols.symbols().isEmpty()) {
|
||||
|
||||
@ -8,10 +8,17 @@ import org.eclipse.lsp4j.SignatureHelp;
|
||||
import org.eclipse.lsp4j.SemanticTokens;
|
||||
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.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.BaselinePrepareRename;
|
||||
import p.studio.lsp.messages.BaselineRename;
|
||||
import p.studio.lsp.messages.BaselineCompletion;
|
||||
import p.studio.lsp.messages.BaselineDefinition;
|
||||
import p.studio.lsp.messages.BaselineReferences;
|
||||
@ -40,6 +47,11 @@ public interface ProtocolMessageMapper {
|
||||
|
||||
List<SymbolInformation> mapWorkspaceSymbols(BaselineWorkspaceSymbols symbols);
|
||||
|
||||
Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
BaselinePrepareRename prepareRename);
|
||||
|
||||
WorkspaceEdit mapRename(BaselineRename rename);
|
||||
|
||||
CompletionList mapCompletion(BaselineCompletion completion);
|
||||
|
||||
SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp);
|
||||
|
||||
@ -59,6 +59,12 @@ class CompilerLanguageServiceBridgeTest {
|
||||
final var workspaceSymbols = bridge.workspaceSymbols(context, "frame");
|
||||
assertTrue(workspaceSymbols.symbols().isEmpty());
|
||||
|
||||
final var prepareRename = bridge.prepareRename(context, documentUri, "fn main() {}", 0, 0);
|
||||
assertFalse(prepareRename.renameable());
|
||||
final var rename = bridge.rename(context, documentUri, "fn main() {}", 0, 0, "other");
|
||||
assertFalse(rename.applied());
|
||||
assertTrue(rename.edits().isEmpty());
|
||||
|
||||
final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}");
|
||||
assertTrue(semanticTokens.legend().isEmpty());
|
||||
assertTrue(semanticTokens.tokens().isEmpty());
|
||||
@ -482,6 +488,73 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(builtinReferences.locations().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void renameRewritesPhysicalFunctionSitesAndRefusesStdlib() {
|
||||
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 = """
|
||||
import { Gfx } from @sdk:gfx;
|
||||
|
||||
fn helper() -> int { return 42; }
|
||||
|
||||
fn frame() -> void {
|
||||
helper();
|
||||
helper();
|
||||
Gfx.clear(5);
|
||||
let n: int = 1;
|
||||
// helper
|
||||
let text = "helper";
|
||||
}
|
||||
""";
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
|
||||
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
|
||||
final String documentUri = documentPath.toUri().toString();
|
||||
|
||||
final var helperUse = mapper.positionOf(overlay.indexOf("helper();"));
|
||||
final var prepared = bridge.prepareRename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
helperUse.line(),
|
||||
helperUse.character());
|
||||
assertTrue(prepared.renameable(), prepared.toString());
|
||||
|
||||
final var renamed = bridge.rename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
helperUse.line(),
|
||||
helperUse.character(),
|
||||
"aid");
|
||||
assertTrue(renamed.applied(), renamed.message());
|
||||
assertEquals(3, renamed.edits().size(), renamed.edits().toString());
|
||||
assertTrue(renamed.edits().stream().allMatch(edit -> edit.newText().equals("aid")));
|
||||
assertTrue(renamed.edits().stream().allMatch(edit -> edit.uri().equals(documentUri)));
|
||||
|
||||
final var clearUse = mapper.positionOf(overlay.indexOf("clear"));
|
||||
final var stdlibPrepared = bridge.prepareRename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
clearUse.line(),
|
||||
clearUse.character());
|
||||
assertFalse(stdlibPrepared.renameable());
|
||||
final var stdlibRename = bridge.rename(
|
||||
context,
|
||||
documentUri,
|
||||
overlay,
|
||||
clearUse.line(),
|
||||
clearUse.character(),
|
||||
"wipe");
|
||||
assertFalse(stdlibRename.applied());
|
||||
assertTrue(stdlibRename.edits().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentSymbolsUseAstHierarchyOfTheOpenPbsFile() {
|
||||
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
|
||||
@ -593,6 +666,7 @@ class CompilerLanguageServiceBridgeTest {
|
||||
assertTrue(description.referencesSupported());
|
||||
assertTrue(description.documentSymbolsSupported());
|
||||
assertTrue(description.workspaceSymbolsSupported());
|
||||
assertTrue(description.renameSupported());
|
||||
assertEquals(1, description.semanticHostProjections().size());
|
||||
assertEquals("vscode", description.semanticHostProjections().getFirst().hostId());
|
||||
assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream()
|
||||
|
||||
@ -103,6 +103,7 @@ class PrometeuLanguageServerTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of(),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -186,6 +187,27 @@ class PrometeuLanguageServerTest {
|
||||
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return p.studio.lsp.messages.BaselinePrepareRename.none();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext project,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
return p.studio.lsp.messages.BaselineRename.unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext project,
|
||||
@ -255,6 +277,20 @@ class PrometeuLanguageServerTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -224,6 +224,27 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
return p.studio.lsp.messages.BaselinePrepareRename.none();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
return p.studio.lsp.messages.BaselineRename.unsupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaselineSignatureHelp signatureHelp(
|
||||
final LspProjectContext context,
|
||||
@ -303,6 +324,20 @@ final class PrometeuTextDocumentServiceTest {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
return new org.eclipse.lsp4j.WorkspaceEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
return completionList;
|
||||
|
||||
@ -145,6 +145,27 @@ final class PrometeuWorkspaceServiceTest {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselinePrepareRename prepareRename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public p.studio.lsp.messages.BaselineRename rename(
|
||||
final LspProjectContext context,
|
||||
final String documentUri,
|
||||
final String text,
|
||||
final int line,
|
||||
final int character,
|
||||
final String newName) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onSave(final LspProjectContext context, final String documentUri) {
|
||||
throw new UnsupportedOperationException();
|
||||
@ -196,6 +217,20 @@ final class PrometeuWorkspaceServiceTest {
|
||||
return workspaceSymbols;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.jsonrpc.messages.Either3<
|
||||
org.eclipse.lsp4j.Range,
|
||||
org.eclipse.lsp4j.PrepareRenameResult,
|
||||
org.eclipse.lsp4j.PrepareRenameDefaultBehavior> mapPrepareRename(
|
||||
final p.studio.lsp.messages.BaselinePrepareRename prepareRename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.WorkspaceEdit mapRename(final p.studio.lsp.messages.BaselineRename rename) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@ -46,6 +46,7 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
List.of("demo-keyword"),
|
||||
List.of(new BaselineSemanticHostProjection(
|
||||
"vscode",
|
||||
@ -70,6 +71,7 @@ final class Lsp4jProtocolMessageMapperTest {
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getWorkspaceSymbolProvider().getLeft());
|
||||
assertEquals(Boolean.TRUE, result.getCapabilities().getRenameProvider().getRight().getPrepareProvider());
|
||||
|
||||
final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental());
|
||||
final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections"));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user