diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 271e3515..b47fb2ec 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":55,"PLN":134,"LSN":71,"CLSN":1}} +{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":56,"PLN":135,"LSN":72,"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"}]} @@ -22,7 +22,7 @@ {"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-0042","status":"open","ticket":"pbs-lsp-workspace-symbols","title":"PBS LSP Workspace Symbols","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["studio","lsp","vscode","compiler-pbs","editor","workspace-symbols"],"agendas":[{"id":"AGD-0045","file":"AGD-0045-pbs-lsp-workspace-symbols.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]} +{"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"}]} {"type":"discussion","id":"DSC-0039","status":"done","ticket":"pbs-lsp-go-to-definition","title":"PBS LSP Go to Definition","created_at":"2026-07-15","updated_at":"2026-09-21","tags":["studio","lsp","vscode","compiler-pbs","editor","definition"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0068","file":"discussion/lessons/DSC-0039-pbs-lsp-go-to-definition/LSN-0068-pbs-go-to-definition-uses-editorial-lookup-and-physical-files.md","status":"done","created_at":"2026-09-21","updated_at":"2026-09-21"}]} diff --git a/discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md b/discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md new file mode 100644 index 00000000..f453b271 --- /dev/null +++ b/discussion/lessons/DSC-0042-pbs-lsp-workspace-symbols/LSN-0071-pbs-workspace-symbols-are-a-physical-named-declaration-search.md @@ -0,0 +1,149 @@ +--- +id: LSN-0071 +ticket: pbs-lsp-workspace-symbols +title: PBS workspace symbols are a physical named-declaration search +created: 2026-09-21 +tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols] +--- + +# PBS workspace symbols are a physical named-declaration search + +## Original Problem + +PBS already had compiler-backed completion, hover, signature help, go-to-definition (`LSN-0068`), find-references (`LSN-0069`), and an AST-backed outline of the open `.pbs` file (`LSN-0070`). VS Code workspace search (`Ctrl+T` / `workspace/symbol`) stayed empty. + +The risk was treating workspace symbols as a second outline, inventing a `SymbolId` index, waiting for snapshot cache (`AGD-0051`), or returning stdlib/SDK hits whose declarations are still virtual paths (`/virtual/stdlib`). Workspace symbols is a jump. Outline is not. + +## Consolidated Decision + +PBS Workspace Symbols is the LSP projection of a **flat named-declaration search** over **regular filesystem files** of the current analysis, flattening the **same outline set** and using the **same physical-file destination policy** as definition. + +Durable locks from `DEC-0055` (AGD-0045 Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions): + +1. Emit only declarations whose `fileTable` handle is a regular filesystem file. Virtual stdlib/SDK, including `/virtual/stdlib`, is omitted. Do not invent virtual documents, `untitled:` buffers, or synthetic URIs. +2. `workspace/symbol` does not require an open text document. The server obtains the project analysis surface (`astByFile` / `projectSurface`) from `analyze()`. Missing surface → empty list. +3. The set is the flatten, file by file, of the named-declaration tree locked by outline (`LSN-0070`): + - `fn` → Function; + - `declare struct` → Struct plus fields, `ctor`s, methods; + - `declare service` → Service plus methods; + - `declare contract` → Contract plus signatures; + - `declare host` → Host plus signatures; + - `declare builtin type` → Builtin type plus projection fields and signatures; + - `declare enum` → Enum plus cases; + - `declare error` → Error plus labels; + - `declare callback` → Callback; + - `declare global` / `const` → Global / Const; + - `implements Contract for Owner` → `contractName`, kind CONTRACT, `detail` = owner, plus methods. + Locals, parameters, `this`, imports, and comments stay out. Private fields stay in. Overloads stay sibling hits. Unnamed `InvalidDecl` is omitted. Named decls survive later semantic errors. +4. First wave is `.pbs` whose recovered root is `PbsAst.File`. `mod.barrel` (`BarrelFile`) stays empty. Presence, name, kind, `detail`, and spans come from recovered AST. Do not invent nodes from text search. +5. Query is a case-insensitive substring of `name` only. Blank query returns the full first-wave corpus. Do not match `detail`, path, `containerName`, or qualified `Vec.blend`. No fuzzy ranking, no `top N`. After the filter, order is stable by name, then path, then start offset. +6. `containerName` is the parent name for members and empty for top-level declarations. `detail` may repeat the outline AST signature. Attributes such as `[Init]` do not enter the name. +7. Keep the capability on aggregated `FrontendLanguageService`. Add `workspaceSymbols` (default empty) plus a flat DTO with `name`, `kind`, optional `detail`, `containerName`, absolute path, and offsets. Do not reuse pathless `FrontendDocumentSymbol`. PBS flattens in `prometeu-frontend-pbs`. Common LSP must not import `p.studio.compiler.pbs.*`. No Workspace Symbol SPI, `SymbolId`, `RefIndex`, or snapshot cache. +8. Advertise `workspaceSymbolProvider` when the frontend exposes a language service. Payload is flat `SymbolInformation`. Do not emit hierarchical `DocumentSymbol` on this request. Compile-only frontends stay empty. VS Code stays a thin client. + +Spec 23 §8.3 now also states: workspace symbols are a flat search of named declarations in compiler-known physical source files of the current analysis; recovered syntax is enough; a missing analysis surface is empty; the request must not require an open document; a declaration without a physical file is omitted; this is not outline, folding, a persistent index, or navigation to virtual locations. + +## Final Implementation + +| Layer | What landed | +|---|---| +| PBS editorial | `PbsAstWorkspaceSymbols` flattens `PbsAstDocumentSymbols` and filters by name. `PbsEditorialSupportService.workspaceSymbols(ast, query)` delegates to that flatten. | +| Generic contract | `FrontendWorkspaceRequest` (`projectRoot`, `query`). `FrontendWorkspaceSymbol` carries path. `FrontendLanguageService.workspaceSymbols` defaults to `List.of()`. `prepareEditorialContext(FrontendPhaseContext)` defaults to empty so a workspace request can obtain project surface without a `FileId`. | +| PBS mapping | Project-level `prepareEditorialContext` attaches `fileTable` + `astByFile`. Regular `PbsAst.File` entries are flattened; non-regular handles are skipped. Results sort by name, path, offset. | +| LSP | `workspaceSymbolProvider`, `workspace/symbol` on `PrometeuWorkspaceService` (no document URI). `CompilerLanguageServiceBridge` analyzes the project with an empty overlay map. Mapper emits flat `SymbolInformation`. Nested enum/error hits with a container project to `EnumMember`. Compile-only frontends stay empty. | +| VS Code | Unchanged thin client. | + +`workspace/symbol` in this wave reads the project on disk. Unsaved editor overlays are not shared into the search. Empty is honest until a later overlay/snapshot decision. + +`mod.barrel` is still `BarrelFile`, not `PbsAst.File`. Empty barrel hits are the honest first-wave result. + +Error case labels still have no per-label span. Flattened children reuse the parent `ErrorDecl` span, same as outline. + +## Examples + +Workspace search of a project file is the flatten of that file's outline, not imports or locals: + +```pbs +import { Log } from @sdk:log; + +fn helper() -> int { return 42; } + +fn frame() -> void { + let local = 1; + helper(local); +} +``` + +Query `frame` → `frame`. Query `helper` → `helper`. Query empty → `helper`, `frame` (plus other physical decls), sorted. Not `Log`, not `local`. + +Members keep a container; overloads stay siblings: + +```pbs +declare struct Vec(x: int) { + ctor make(x: int) { return; } + fn blend(dx: int, dy: int) -> int { return dx; } +} + +fn helper() -> int { return 42; } +fn helper(value: int) -> int { return value; } +``` + +Query `blend` → method `blend` with `containerName` `Vec`. Query `helper` → two hits. Query `Vec.blend` → empty (this wave matches `name` only). + +Stdlib remains hover-only while the handle is virtual: + +```pbs +import { Gfx } from @sdk:gfx; +Gfx.clear(0); +``` + +Query `Gfx` is empty. F12 on `Gfx.clear` is already empty (`LSN-0068`). Workspace search does not reopen that jump. + +Recovered parse keeps the named decl: + +```pbs +declare ; +fn ok() -> int { return 1; } +``` + +Query empty → `ok`. The unnamed `InvalidDecl` is omitted. + +`[Init] fn init()` still names the symbol `init`. + +## Pitfalls + +Do not implement workspace symbols as workspace text search in `lsp-v1` or in the VS Code extension. It will lie about overloads, members, and imports. + +Do not reuse `FrontendDocumentSymbol`. Outline is pathless because the host already has the document URI. Workspace symbols is a jump and must carry a path. + +Do not require an open editor buffer to answer `workspace/symbol`. Analyze the project and walk `astByFile`. + +Do not include `/virtual/stdlib` so Ctrl+T on `Gfx` “does something”. Empty is the honest result until those sources are regular files. + +Do not wait for snapshot cache. References already walks the current analysis request-locally. Snapshots (`AGD-0051`) can make the walk cheaper without changing this contract. + +Do not add a `SymbolId` index “for rename later”. Outline already named the declarations; definition already named the destination. + +Do not match `containerName`, path, or `Vec.blend` in this wave. The query is a substring of `name`. + +Do not emit hierarchical `DocumentSymbol` on `workspace/symbol`. That payload is outline. + +Do not treat unsaved overlay absence as a missing feature of this contract. First-wave analysis is the project on disk. + +Do not split a Workspace Symbol SPI “for future languages”. PBS is a real consumer of `FrontendLanguageService` (`LSN-0067`). + +Do not treat this lesson as closing rename, snapshots, folding, document links, or hierarchy. + +## References + +- Agenda: `AGD-0045` (Q1 A, Q2 A, Q3 A, Q4 A, Q5 A, Q6 first-wave exclusions) +- Decision: `DEC-0055` +- Plan: `PLN-0134` +- Spec 23 §8.3 — optional workspace symbols; physical-file-or-omit; no open document required +- `LSN-0070` — outline set this feature flattens +- `LSN-0068` — physical-file destination this feature reuses +- `LSN-0069` — project `astByFile` walk already exists for references +- `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-0046` rename, `AGD-0051` snapshots, `AGD-0053` folding, `AGD-0055` document links, `AGD-0056` hierarchy diff --git a/discussion/workflow/agendas/AGD-0045-pbs-lsp-workspace-symbols.md b/discussion/workflow/agendas/AGD-0045-pbs-lsp-workspace-symbols.md deleted file mode 100644 index 363fe79e..00000000 --- a/discussion/workflow/agendas/AGD-0045-pbs-lsp-workspace-symbols.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: AGD-0045 -ticket: pbs-lsp-workspace-symbols -title: PBS LSP Workspace Symbols -status: open -created: 2026-07-15 -resolved: -decision: -tags: [studio, lsp, vscode, compiler-pbs, editor, workspace-symbols] ---- - -## Pain - -Domain owner: `studio/lsp` - -VS Code cannot search PBS symbols across the workspace, so users cannot quickly jump to functions, services, structs, constants, or stdlib-exposed APIs by name. - -## Context - -Document-level editorial features exist, but the LSP does not announce workspaceSymbolProvider and the compiler pipeline does not expose a project-wide symbol index designed for editor queries. - -## Open Questions - -- [ ] Should workspace symbols include only project files or also imported stdlib and SDK surfaces? -- [ ] What symbol kinds and ranking should be returned for duplicate names or overloads? -- [ ] Should the symbol index be cached from analysis snapshots or rebuilt per request in the first implementation? - -## Options - -### Option A - Project source index only - -- **Approach:** Index project-owned declarations from analysis snapshots and expose `workspace/symbol`. -- **Pro:** Clear ownership and physical locations. -- **Con:** Does not help users discover stdlib/SDK symbols. -- **Maintainability:** Strong as a first wave. - -### Option B - Project plus stdlib index - -- **Approach:** Include project declarations and imported stdlib/SDK surfaces in the workspace symbol result set. -- **Pro:** Better discovery, especially for new users. -- **Con:** Requires a navigation policy for stdlib source or virtual targets. -- **Maintainability:** Good if stdlib target handling is solved; awkward otherwise. - -## Tradeoffs - -Workspace symbols are partly navigation and partly discovery. Including stdlib too early can create broken jumps if target locations are not defined. - -## Recommendation - -Prefer Option A first, then add stdlib once definition has a target policy. - -## Discussion - -This should follow document symbols and the semantic location/index work. - -## Resolution - -Ainda em aberto. - -## Next Step - -Decide whether first-wave workspace search includes stdlib or project-only symbols. diff --git a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md index 2507bc6c..740ff011 100644 --- a/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md +++ b/docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md @@ -278,6 +278,8 @@ When a frontend exposes find-references, each returned usage or declaration loca When a frontend exposes document symbols or outline, the result MUST be the named declaration tree of the requested document. Presence, hierarchy, and ranges MUST come from that document's recovered syntax tree. Semantic success MUST NOT be required. Semantic enrichment MUST NOT drop a named declaration that the syntax tree still exposes. A missing syntax tree MUST produce an empty list. Document symbols MUST NOT be a workspace symbol index, folding ranges, or a navigation list of foreign files. +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. + 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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java index a20077c7..774946ac 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendLanguageService.java @@ -13,6 +13,7 @@ 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.PbsEditorialWorkspaceSymbol; import p.studio.compiler.services.PBSFrontendPhaseService.PbsSemanticReadSurface; import p.studio.compiler.services.FrontendCompletionCandidate; import p.studio.compiler.services.FrontendDefinitionLocation; @@ -25,6 +26,8 @@ import p.studio.compiler.services.FrontendLanguageService; import p.studio.compiler.services.FrontendSignature; import p.studio.compiler.services.FrontendSignatureHelp; import p.studio.compiler.services.FrontendSymbolKind; +import p.studio.compiler.services.FrontendWorkspaceRequest; +import p.studio.compiler.services.FrontendWorkspaceSymbol; import p.studio.compiler.services.PBSFrontendPhaseService; import p.studio.compiler.source.Span; import p.studio.compiler.source.diagnostics.DiagnosticSink; @@ -35,6 +38,7 @@ import p.studio.utilities.structures.ReadOnlyList; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; @@ -55,6 +59,23 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService this.semanticTokenProvider = Objects.requireNonNull(semanticTokenProvider, "semanticTokenProvider"); } + @Override + public Optional prepareEditorialContext(final FrontendPhaseContext phaseContext) { + if (phaseContext == null) { + return Optional.empty(); + } + final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface( + phaseContext, + DiagnosticSink.empty(), + BuildingIssueSink.empty()); + return Optional.of(new FrontendEditorialContext( + null, + ReadOnlyList.empty(), + phaseContext.feSurfaceContext(), + phaseContext.fileTable, + semanticReadSurface)); + } + @Override public Optional prepareEditorialContext( final FrontendPhaseContext phaseContext, @@ -281,6 +302,41 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService .toList(); } + @Override + public List workspaceSymbols( + final FrontendWorkspaceRequest request, + final FrontendEditorialContext editorialContext) { + if (request == null + || editorialContext == null + || !(editorialContext.sourceFiles() instanceof FileTableReader fileTable) + || !(editorialContext.projectSurface() instanceof PbsSemanticReadSurface surface) + || surface.astByFile() == null + || surface.astByFile().isEmpty()) { + return List.of(); + } + final ArrayList symbols = new ArrayList<>(); + for (final var entry : surface.astByFile().entrySet()) { + final Path path = pathForRegularSource(entry.getKey(), fileTable); + if (path == null) { + continue; + } + final PbsAst.File fileAst = entry.getValue(); + if (fileAst == null) { + continue; + } + for (final PbsEditorialWorkspaceSymbol symbol : editorialSupportService.workspaceSymbols( + fileAst, + request.query())) { + toFrontendWorkspaceSymbol(symbol, path).ifPresent(symbols::add); + } + } + symbols.sort(Comparator + .comparing(FrontendWorkspaceSymbol::name, String.CASE_INSENSITIVE_ORDER) + .thenComparing(symbol -> symbol.path().toString()) + .thenComparingInt(FrontendWorkspaceSymbol::startOffset)); + return List.copyOf(symbols); + } + @Override public List semanticTokens(final FrontendDocumentRequest request) { return semanticTokens(request == null ? "" : request.documentText()); @@ -328,6 +384,22 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return FrontendSymbolKind.valueOf(kind.name()); } + private Optional toFrontendWorkspaceSymbol( + final PbsEditorialWorkspaceSymbol symbol, + final Path path) { + if (symbol == null || path == null || symbol.span() == null || symbol.span().isNone()) { + return Optional.empty(); + } + return Optional.of(new FrontendWorkspaceSymbol( + symbol.name(), + toFrontendSymbolKind(symbol.kind()), + symbol.detail(), + symbol.containerName(), + path, + toOffset(symbol.span().getStart()), + toOffset(symbol.span().getEnd()))); + } + private FrontendDocumentSymbol toFrontendDocumentSymbol(final PbsEditorialDocumentSymbol symbol) { final int start = toOffset(symbol.span().getStart()); final int end = toOffset(symbol.span().getEnd()); @@ -376,6 +448,15 @@ public final class PBSFrontendLanguageService implements FrontendLanguageService return List.copyOf(documents); } + private Path pathForRegularSource( + final FileId fileId, + final FileTableReader fileTable) { + if (!isRegularSource(fileId, fileTable)) { + return null; + } + return fileTable.get(fileId).getCanonPath().toAbsolutePath().normalize(); + } + private boolean isRegularSource( final FileId fileId, final FileTableReader fileTable) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsAstWorkspaceSymbols.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsAstWorkspaceSymbols.java new file mode 100644 index 00000000..d1250e6d --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsAstWorkspaceSymbols.java @@ -0,0 +1,55 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.pbs.ast.PbsAst; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +final class PbsAstWorkspaceSymbols { + private PbsAstWorkspaceSymbols() { + } + + static List list( + final PbsAst.File ast, + final String query) { + if (ast == null) { + return List.of(); + } + final ArrayList symbols = new ArrayList<>(); + for (final PbsEditorialDocumentSymbol topDecl : PbsAstDocumentSymbols.list(ast)) { + flatten(topDecl, "", query, symbols); + } + return List.copyOf(symbols); + } + + private static void flatten( + final PbsEditorialDocumentSymbol symbol, + final String containerName, + final String query, + final ArrayList symbols) { + if (matches(symbol.name(), query)) { + symbols.add(new PbsEditorialWorkspaceSymbol( + symbol.name(), + symbol.kind(), + symbol.detail(), + containerName, + symbol.span())); + } + for (final PbsEditorialDocumentSymbol child : symbol.children()) { + flatten(child, symbol.name(), query, symbols); + } + } + + static boolean matches( + final String name, + final String query) { + if (name == null || name.isBlank()) { + return false; + } + if (query == null || query.isBlank()) { + return true; + } + return name.toLowerCase(Locale.ROOT).contains(query.trim().toLowerCase(Locale.ROOT)); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java index a6381d5d..cdc292ee 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java @@ -78,6 +78,12 @@ public final class PbsEditorialSupportService { return PbsAstDocumentSymbols.list(ast); } + public List workspaceSymbols( + final PbsAst.File ast, + final String query) { + return PbsAstWorkspaceSymbols.list(ast, query); + } + public List definition( final String sourceText, final PbsAst.File ast, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialWorkspaceSymbol.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialWorkspaceSymbol.java new file mode 100644 index 00000000..2a1c099c --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialWorkspaceSymbol.java @@ -0,0 +1,21 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.source.Span; + +import java.util.Objects; + +public record PbsEditorialWorkspaceSymbol( + String name, + PbsEditorialSymbolKind kind, + String detail, + String containerName, + Span span) { + + public PbsEditorialWorkspaceSymbol { + name = Objects.requireNonNull(name, "name").trim(); + kind = Objects.requireNonNull(kind, "kind"); + detail = detail == null ? "" : detail; + containerName = containerName == null ? "" : containerName.trim(); + span = Objects.requireNonNull(span, "span"); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java index fced3355..c85c0d61 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java @@ -526,6 +526,51 @@ final class PbsEditorialSupportServiceTest { assertEquals(List.of("tick"), names(implementsSymbol.children())); } + @Test + void shouldFlattenOutlineMembersForWorkspaceSearchAndFilterByName() { + final var source = """ + import { Log } from @sdk:log; + + declare struct Vec(x: int) { + ctor make(x: int) { return; } + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn helper() -> int { return 42; } + fn helper(value: int) -> int { return value; } + + fn frame() -> void { + let local = 1; + helper(local); + } + """; + final var ast = parseOrdinary(source); + final var all = service.workspaceSymbols(ast, ""); + assertEquals(List.of( + "Vec", + "x", + "make", + "blend", + "helper", + "helper", + "frame"), all.stream().map(PbsEditorialWorkspaceSymbol::name).toList()); + assertEquals("", all.getFirst().containerName()); + assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("blend")).findFirst().orElseThrow().containerName()); + assertEquals("Vec", all.stream().filter(symbol -> symbol.name().equals("x")).findFirst().orElseThrow().containerName()); + assertTrue(all.stream().noneMatch(symbol -> symbol.name().equals("Log") || symbol.name().equals("local"))); + + final var blendHits = service.workspaceSymbols(ast, "BLEND"); + assertEquals(List.of("blend"), blendHits.stream().map(PbsEditorialWorkspaceSymbol::name).toList()); + assertEquals("Vec", blendHits.getFirst().containerName()); + + final var helperHits = service.workspaceSymbols(ast, "helper"); + assertEquals(2, helperHits.size()); + assertNotEquals(helperHits.getFirst().detail(), helperHits.get(1).detail()); + + final var qualifiedMiss = service.workspaceSymbols(ast, "Vec.blend"); + assertTrue(qualifiedMiss.isEmpty()); + } + @Test void shouldOmitInvalidDeclarationsAndKeepNamedDeclsFromRecoveredAst() { final var source = """ @@ -536,6 +581,8 @@ final class PbsEditorialSupportServiceTest { final var symbols = service.documentSymbols(ast); assertEquals(List.of("ok"), names(symbols)); assertEquals(PbsEditorialSymbolKind.FUNCTION, symbols.getFirst().kind()); + final var workspaceSymbols = service.workspaceSymbols(ast, ""); + assertEquals(List.of("ok"), workspaceSymbols.stream().map(PbsEditorialWorkspaceSymbol::name).toList()); } @Test diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java index 218d1ade..c9e3ee23 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendLanguageService.java @@ -11,6 +11,11 @@ import java.util.Optional; * Optional frontend-owned editor services. */ public interface FrontendLanguageService { + default Optional prepareEditorialContext( + final FrontendPhaseContext phaseContext) { + return Optional.empty(); + } + default Optional prepareEditorialContext( final FrontendPhaseContext phaseContext, final FileId fileId) { @@ -95,4 +100,14 @@ public interface FrontendLanguageService { final FrontendEditorialContext editorialContext) { return documentSymbols(request); } + + default List workspaceSymbols(final FrontendWorkspaceRequest request) { + return List.of(); + } + + default List workspaceSymbols( + final FrontendWorkspaceRequest request, + final FrontendEditorialContext editorialContext) { + return workspaceSymbols(request); + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceRequest.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceRequest.java new file mode 100644 index 00000000..e74cbb5e --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceRequest.java @@ -0,0 +1,14 @@ +package p.studio.compiler.services; + +import java.nio.file.Path; +import java.util.Objects; + +public record FrontendWorkspaceRequest( + Path projectRoot, + String query) { + + public FrontendWorkspaceRequest { + projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize(); + query = query == null ? "" : query; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceSymbol.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceSymbol.java new file mode 100644 index 00000000..da120235 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/services/FrontendWorkspaceSymbol.java @@ -0,0 +1,38 @@ +package p.studio.compiler.services; + +import java.nio.file.Path; +import java.util.Objects; + +public record FrontendWorkspaceSymbol( + String name, + FrontendSymbolKind kind, + String detail, + String containerName, + Path path, + int startOffset, + int endOffset) { + + public FrontendWorkspaceSymbol { + name = requireText(name, "name"); + kind = Objects.requireNonNull(kind, "kind"); + detail = detail == null ? "" : detail.trim(); + containerName = containerName == null ? "" : containerName.trim(); + 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"); + } + } + + 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; + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java index 45354e0e..edc71d7e 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/services/FrontendLanguageServiceTest.java @@ -25,6 +25,8 @@ class FrontendLanguageServiceTest { assertTrue(service.definition(request, 0).isEmpty()); assertTrue(service.references(request, 0).isEmpty()); assertTrue(service.documentSymbols(request).isEmpty()); + assertTrue(service.workspaceSymbols(new FrontendWorkspaceRequest(Path.of("."), "frame")).isEmpty()); + assertTrue(service.prepareEditorialContext(null).isEmpty()); } @Test @@ -90,6 +92,56 @@ class FrontendLanguageServiceTest { List.of())); } + @Test + void workspaceSymbolNormalizesOptionalTextAndRejectsBlankNameOrInvertedOffsets() { + final Path path = Path.of("src/main.pbs").toAbsolutePath(); + final var symbol = new FrontendWorkspaceSymbol( + " blend ", + FrontendSymbolKind.METHOD, + " (dx: int) -> int ", + " Vec ", + path, + 4, + 9); + assertEquals("blend", symbol.name()); + assertEquals("(dx: int) -> int", symbol.detail()); + assertEquals("Vec", symbol.containerName()); + assertEquals(path.normalize(), symbol.path()); + + assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol( + " ", + FrontendSymbolKind.FUNCTION, + "", + "", + path, + 0, + 1)); + assertThrows(NullPointerException.class, () -> new FrontendWorkspaceSymbol( + "fn", + FrontendSymbolKind.FUNCTION, + "", + "", + null, + 0, + 1)); + assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol( + "fn", + FrontendSymbolKind.FUNCTION, + "", + "", + path, + -1, + 0)); + assertThrows(IllegalArgumentException.class, () -> new FrontendWorkspaceSymbol( + "fn", + FrontendSymbolKind.FUNCTION, + "", + "", + path, + 5, + 4)); + } + @Test void definitionLocationRejectsBlankPathAndInvertedOffsets() { final Path path = Path.of("src/main.pbs").toAbsolutePath(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index 3a3c3523..2615ea2c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -13,6 +13,7 @@ public record BaselineServerDescription( boolean definitionSupported, boolean referencesSupported, boolean documentSymbolsSupported, + boolean workspaceSymbolsSupported, List semanticTokenTypes, List semanticHostProjections, List visualThemes, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbol.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbol.java new file mode 100644 index 00000000..532f9bc5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbol.java @@ -0,0 +1,36 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineWorkspaceSymbol( + String name, + BaselineDocumentSymbolKind kind, + String detail, + String containerName, + String uri, + int startLine, + int startCharacter, + int endLine, + int endCharacter) { + + public BaselineWorkspaceSymbol { + name = requireText(name, "name"); + kind = Objects.requireNonNull(kind, "kind"); + detail = detail == null ? "" : detail; + containerName = containerName == null ? "" : containerName; + uri = requireText(uri, "uri"); + if (startLine < 0 || startCharacter < 0 || endLine < 0 || endCharacter < 0) { + throw new IllegalArgumentException("workspace symbol range coordinates must not be negative"); + } + } + + 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; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbols.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbols.java new file mode 100644 index 00000000..eaa2ee22 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineWorkspaceSymbols.java @@ -0,0 +1,15 @@ +package p.studio.lsp.messages; + +import java.util.List; + +public record BaselineWorkspaceSymbols( + List symbols) { + + public BaselineWorkspaceSymbols { + symbols = symbols == null ? List.of() : List.copyOf(symbols); + } + + public static BaselineWorkspaceSymbols empty() { + return new BaselineWorkspaceSymbols(List.of()); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java index 35cd4d84..d6f96dc2 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -6,6 +6,7 @@ import p.studio.lsp.messages.BaselineDefinition; 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.BaselineSemanticTokens; import p.studio.lsp.messages.BaselineSignatureHelp; import p.studio.lsp.messages.BaselineServerDescription; @@ -32,6 +33,8 @@ public interface LanguageServiceBridge { BaselineDocumentSymbols documentSymbols(LspProjectContext context, String documentUri, String text); + BaselineWorkspaceSymbols workspaceSymbols(LspProjectContext context, String query); + BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character); BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 043b69be..2ce0ff0f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -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.FrontendWorkspaceRequest; +import p.studio.compiler.services.FrontendWorkspaceSymbol; import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.utilities.SourceProviderFactory; import p.studio.compiler.workspaces.AssetSurfaceContextLoader; @@ -57,6 +59,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(), @@ -235,6 +238,40 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg .orElseGet(BaselineDocumentSymbols::empty); } + @Override + public BaselineWorkspaceSymbols workspaceSymbols( + final LspProjectContext context, + final String query) { + Objects.requireNonNull(context, "context"); + final var provider = frontendProvider(context); + final var languageService = provider.languageService(); + if (languageService.isEmpty()) { + return BaselineWorkspaceSymbols.empty(); + } + final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig( + new BuilderPipelineConfig( + false, + context.projectRoot().toString(), + "core-v1", + SourceProviderFactory.overlayUtf8(Map.of()))); + BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty()); + final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( + pipelineContext.resolvedWorkspace.graph().projectTable(), + pipelineContext.fileTable, + pipelineContext.resolvedWorkspace.stack(), + pipelineContext.resolvedWorkspace.stdlib(), + pipelineContext.resolvedWorkspace.target(), + HostAdmissionContext.permissiveDefault(), + assetSurfaceContextLoader.load(pipelineContext.resolvedWorkspace.mainProject().getRootPath())); + return languageService.orElseThrow() + .prepareEditorialContext(frontendPhaseContext) + .map(editorialContext -> new BaselineWorkspaceSymbols(mapWorkspaceSymbols( + languageService.orElseThrow().workspaceSymbols( + new FrontendWorkspaceRequest(context.projectRoot(), query), + editorialContext)))) + .orElseGet(BaselineWorkspaceSymbols::empty); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -558,6 +595,44 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg children); } + private List mapWorkspaceSymbols(final List symbols) { + if (symbols == null || symbols.isEmpty()) { + return List.of(); + } + final LinkedHashMap mappers = new LinkedHashMap<>(); + final ArrayList mapped = new ArrayList<>(); + for (final FrontendWorkspaceSymbol symbol : symbols) { + final DocumentPositionMapper mapper = mappers.computeIfAbsent(symbol.path(), this::mapperForPath); + if (mapper == null) { + continue; + } + final DocumentPosition start = mapper.positionOf(symbol.startOffset()); + final DocumentPosition end = mapper.positionOf(symbol.endOffset()); + mapped.add(new BaselineWorkspaceSymbol( + symbol.name(), + BaselineDocumentSymbolKind.valueOf(symbol.kind().name()), + symbol.detail(), + symbol.containerName(), + symbol.path().toUri().toString(), + start.line(), + start.character(), + end.line(), + end.character())); + } + return List.copyOf(mapped); + } + + private DocumentPositionMapper mapperForPath(final Path path) { + if (path == null || !Files.isRegularFile(path)) { + return null; + } + try { + return new DocumentPositionMapper(Files.readString(path, StandardCharsets.UTF_8)); + } catch (final Exception ignored) { + return null; + } + } + private List mapDefinitionLocations( final List locations, final Path currentDocumentPath, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java index 58b9aff1..18c4be4f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java @@ -29,7 +29,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli compilerBridge, new Lsp4jProtocolMessageMapper(), null, - new PrometeuWorkspaceService()); + null); } PrometeuLanguageServer( @@ -44,7 +44,9 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli this.textDocumentService = textDocumentService != null ? textDocumentService : new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper); - this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService"); + this.workspaceService = workspaceService != null + ? workspaceService + : new PrometeuWorkspaceService(project, languageServiceBridge, protocolMessageMapper); } @Override diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java index 612f9dd7..7546e424 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java @@ -2,9 +2,36 @@ package p.studio.lsp.services.protocol; import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.WorkspaceService; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.util.List; +import java.util.concurrent.CompletableFuture; public final class PrometeuWorkspaceService implements WorkspaceService { + private final LspProjectContext project; + private final LanguageServiceBridge languageServiceBridge; + private final ProtocolMessageMapper protocolMessageMapper; + + public PrometeuWorkspaceService() { + this(null, null, null); + } + + public PrometeuWorkspaceService( + final LspProjectContext project, + final LanguageServiceBridge languageServiceBridge, + final ProtocolMessageMapper protocolMessageMapper) { + this.project = project; + this.languageServiceBridge = languageServiceBridge; + this.protocolMessageMapper = protocolMessageMapper; + } + @Override public void didChangeConfiguration(final DidChangeConfigurationParams params) { } @@ -12,4 +39,15 @@ public final class PrometeuWorkspaceService implements WorkspaceService { @Override public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) { } + + @Override + public CompletableFuture, List>> symbol( + final WorkspaceSymbolParams params) { + if (project == null || languageServiceBridge == null || protocolMessageMapper == null) { + return CompletableFuture.completedFuture(Either.forLeft(List.of())); + } + final String query = params == null || params.getQuery() == null ? "" : params.getQuery(); + return CompletableFuture.completedFuture(Either.forLeft( + protocolMessageMapper.mapWorkspaceSymbols(languageServiceBridge.workspaceSymbols(project, query)))); + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index de2f50a0..6024b623 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -39,6 +39,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { capabilities.setDefinitionProvider(description.definitionSupported()); capabilities.setReferencesProvider(description.referencesSupported()); capabilities.setDocumentSymbolProvider(description.documentSymbolsSupported()); + capabilities.setWorkspaceSymbolProvider(description.workspaceSymbolsSupported()); final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); @@ -114,6 +115,27 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return List.copyOf(mapped); } + @Override + public List mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) { + if (symbols == null || symbols.symbols().isEmpty()) { + return List.of(); + } + final ArrayList mapped = new ArrayList<>(); + for (final BaselineWorkspaceSymbol symbol : symbols.symbols()) { + final SymbolInformation information = new SymbolInformation(); + information.setName(symbol.name()); + information.setKind(toLspWorkspaceSymbolKind(symbol.kind(), symbol.containerName())); + information.setContainerName(symbol.containerName()); + information.setLocation(new Location( + symbol.uri(), + new Range( + new Position(symbol.startLine(), symbol.startCharacter()), + new Position(symbol.endLine(), symbol.endCharacter())))); + mapped.add(information); + } + return List.copyOf(mapped); + } + private DocumentSymbol mapDocumentSymbol( final BaselineDocumentSymbol symbol, final BaselineDocumentSymbolKind parentKind) { @@ -135,6 +157,17 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { return mapped; } + private SymbolKind toLspWorkspaceSymbolKind( + final BaselineDocumentSymbolKind kind, + final String containerName) { + if (containerName != null + && !containerName.isBlank() + && (kind == BaselineDocumentSymbolKind.ENUM || kind == BaselineDocumentSymbolKind.ERROR)) { + return SymbolKind.EnumMember; + } + return toLspSymbolKind(kind, null); + } + private SymbolKind toLspSymbolKind( final BaselineDocumentSymbolKind kind, final BaselineDocumentSymbolKind parentKind) { diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java index bbbb9391..35386531 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -8,8 +8,10 @@ import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SemanticTokens; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.SymbolInformation; import p.studio.lsp.messages.BaselineDocumentAnalysis; import p.studio.lsp.messages.BaselineDocumentSymbols; +import p.studio.lsp.messages.BaselineWorkspaceSymbols; import p.studio.lsp.messages.BaselineCompletion; import p.studio.lsp.messages.BaselineDefinition; import p.studio.lsp.messages.BaselineReferences; @@ -36,6 +38,8 @@ public interface ProtocolMessageMapper { List mapDocumentSymbols(BaselineDocumentSymbols symbols); + List mapWorkspaceSymbols(BaselineWorkspaceSymbols symbols); + CompletionList mapCompletion(BaselineCompletion completion); SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index ed7acfcf..86b11b94 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -56,6 +56,9 @@ class CompilerLanguageServiceBridgeTest { final var documentSymbols = bridge.documentSymbols(context, documentUri, "fn main() {}"); assertTrue(documentSymbols.symbols().isEmpty()); + final var workspaceSymbols = bridge.workspaceSymbols(context, "frame"); + assertTrue(workspaceSymbols.symbols().isEmpty()); + final var semanticTokens = bridge.semanticTokens(context, documentUri, "fn main() {}"); assertTrue(semanticTokens.legend().isEmpty()); assertTrue(semanticTokens.tokens().isEmpty()); @@ -520,6 +523,62 @@ class CompilerLanguageServiceBridgeTest { assertNotEquals(symbols.get(1).detail(), symbols.get(2).detail()); } + @Test + void workspaceSymbolsSearchPhysicalNamedDeclarationsWithoutAnOpenDocument() { + final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) + .resolve("test-projects") + .resolve("main") + .toAbsolutePath() + .normalize(); + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final var context = new LspProjectContext("main", "pbs", projectRoot); + + final var frameHits = bridge.workspaceSymbols(context, "frame"); + assertTrue(frameHits.symbols().stream().anyMatch(symbol -> + symbol.name().equals("frame") && symbol.containerName().isEmpty())); + assertTrue(frameHits.symbols().stream().allMatch(symbol -> + symbol.uri().contains("main.pbs"))); + + final var caseInsensitive = bridge.workspaceSymbols(context, "BLA"); + assertTrue(caseInsensitive.symbols().stream().anyMatch(symbol -> + symbol.name().equals("Bla") && symbol.containerName().isEmpty())); + + final var all = bridge.workspaceSymbols(context, ""); + assertTrue(all.symbols().stream().anyMatch(symbol -> + symbol.name().equals("a") && symbol.containerName().equals("Bla"))); + assertTrue(all.symbols().stream().anyMatch(symbol -> symbol.name().equals("init"))); + assertTrue(all.symbols().stream().noneMatch(symbol -> + symbol.name().equals("Gfx") + || symbol.name().equals("Log") + || symbol.name().equals("Composer"))); + assertTrue(isSortedByNameThenUriThenOffset(all.symbols())); + + final var stdlibMiss = bridge.workspaceSymbols(context, "Gfx"); + assertTrue(stdlibMiss.symbols().isEmpty()); + } + + private boolean isSortedByNameThenUriThenOffset( + final List symbols) { + for (int index = 1; index < symbols.size(); index += 1) { + final var previous = symbols.get(index - 1); + final var current = symbols.get(index); + final int nameOrder = String.CASE_INSENSITIVE_ORDER.compare(previous.name(), current.name()); + if (nameOrder > 0) { + return false; + } + if (nameOrder == 0) { + final int uriOrder = previous.uri().compareTo(current.uri()); + if (uriOrder > 0) { + return false; + } + if (uriOrder == 0 && previous.startLine() > current.startLine()) { + return false; + } + } + } + return true; + } + @Test void describeServerPublishesFrontendVisualThemes() { final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); @@ -533,6 +592,7 @@ class CompilerLanguageServiceBridgeTest { assertTrue(description.definitionSupported()); assertTrue(description.referencesSupported()); assertTrue(description.documentSymbolsSupported()); + assertTrue(description.workspaceSymbolsSupported()); assertEquals(1, description.semanticHostProjections().size()); assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index dde7f526..95e893e9 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -102,6 +102,7 @@ class PrometeuLanguageServerTest { true, true, true, + true, List.of(), List.of(new BaselineSemanticHostProjection( "vscode", @@ -178,6 +179,13 @@ class PrometeuLanguageServerTest { return p.studio.lsp.messages.BaselineDocumentSymbols.empty(); } + @Override + public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols( + final LspProjectContext project, + final String query) { + return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty(); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext project, @@ -241,6 +249,12 @@ class PrometeuLanguageServerTest { throw new UnsupportedOperationException(); } + @Override + public java.util.List mapWorkspaceSymbols( + final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) { + throw new UnsupportedOperationException(); + } + @Override public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { throw new UnsupportedOperationException(); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java index a6bc0102..953dad9f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java @@ -217,6 +217,13 @@ final class PrometeuTextDocumentServiceTest { List.of()))); } + @Override + public p.studio.lsp.messages.BaselineWorkspaceSymbols workspaceSymbols( + final LspProjectContext context, + final String query) { + return p.studio.lsp.messages.BaselineWorkspaceSymbols.empty(); + } + @Override public BaselineSignatureHelp signatureHelp( final LspProjectContext context, @@ -290,6 +297,12 @@ final class PrometeuTextDocumentServiceTest { return documentSymbols; } + @Override + public List mapWorkspaceSymbols( + final p.studio.lsp.messages.BaselineWorkspaceSymbols symbols) { + return List.of(); + } + @Override public CompletionList mapCompletion(final BaselineCompletion completion) { return completionList; diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuWorkspaceServiceTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuWorkspaceServiceTest.java new file mode 100644 index 00000000..30a27dca --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuWorkspaceServiceTest.java @@ -0,0 +1,229 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineDefinition; +import p.studio.lsp.messages.BaselineDocumentAnalysis; +import p.studio.lsp.messages.BaselineDocumentSymbols; +import p.studio.lsp.messages.BaselineDocumentSymbolKind; +import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineReferences; +import p.studio.lsp.messages.BaselineSemanticTokens; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.BaselineWorkspaceSymbol; +import p.studio.lsp.messages.BaselineWorkspaceSymbols; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class PrometeuWorkspaceServiceTest { + + @Test + void symbolDelegatesQueryWithoutADocumentUri() { + final var bridge = new RecordingBridge(); + final var mapper = new RecordingMapper(); + final var project = new LspProjectContext("main", "pbs", Path.of(".")); + final var service = new PrometeuWorkspaceService(project, bridge, mapper); + + final var result = service.symbol(new WorkspaceSymbolParams("blend")).join(); + + assertTrue(result.isLeft()); + assertSame(mapper.workspaceSymbols, result.getLeft()); + assertEquals("blend", bridge.lastQuery); + assertSame(project, bridge.lastContext); + } + + private static final class RecordingBridge implements LanguageServiceBridge { + private LspProjectContext lastContext; + private String lastQuery; + + @Override + public BaselineServerDescription describeServer(final LspProjectContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineDocumentAnalysis analyzeDocument( + final LspProjectContext context, + final String documentUri, + final String text) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineCompletion completion( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineHover hover( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineDefinition definition( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineReferences references( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character, + final boolean includeDeclaration) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineDocumentSymbols documentSymbols( + final LspProjectContext context, + final String documentUri, + final String text) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineWorkspaceSymbols workspaceSymbols( + final LspProjectContext context, + final String query) { + lastContext = context; + lastQuery = query; + return new BaselineWorkspaceSymbols(List.of(new BaselineWorkspaceSymbol( + "blend", + BaselineDocumentSymbolKind.METHOD, + "", + "Vec", + "file:///tmp/demo.pbs", + 0, + 0, + 0, + 5))); + } + + @Override + public BaselineSignatureHelp signatureHelp( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineSemanticTokens semanticTokens( + final LspProjectContext context, + final String documentUri, + final String text) { + throw new UnsupportedOperationException(); + } + + @Override + public String onSave(final LspProjectContext context, final String documentUri) { + throw new UnsupportedOperationException(); + } + } + + private static final class RecordingMapper implements ProtocolMessageMapper { + private final List workspaceSymbols = List.of(new SymbolInformation()); + + @Override + public org.eclipse.lsp4j.InitializeResult mapInitializeResult(final BaselineServerDescription description) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.PublishDiagnosticsParams mapDiagnostics( + final String uri, + final BaselineDocumentAnalysis analysis) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.PublishDiagnosticsParams emptyDiagnostics(final String uri) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.Hover mapHover(final BaselineHover hover) { + throw new UnsupportedOperationException(); + } + + @Override + public List mapDefinition(final BaselineDefinition definition) { + throw new UnsupportedOperationException(); + } + + @Override + public List mapReferences(final BaselineReferences references) { + throw new UnsupportedOperationException(); + } + + @Override + public List mapDocumentSymbols(final BaselineDocumentSymbols symbols) { + throw new UnsupportedOperationException(); + } + + @Override + public List mapWorkspaceSymbols(final BaselineWorkspaceSymbols symbols) { + return workspaceSymbols; + } + + @Override + public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapErrorMessage(final String message) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapWarningMessage(final String message) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java index c31542d0..273bbbb0 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -45,6 +45,7 @@ final class Lsp4jProtocolMessageMapperTest { true, true, true, + true, List.of("demo-keyword"), List.of(new BaselineSemanticHostProjection( "vscode", @@ -68,6 +69,7 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(Boolean.TRUE, result.getCapabilities().getDefinitionProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getReferencesProvider().getLeft()); assertEquals(Boolean.TRUE, result.getCapabilities().getDocumentSymbolProvider().getLeft()); + assertEquals(Boolean.TRUE, result.getCapabilities().getWorkspaceSymbolProvider().getLeft()); final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); @@ -208,4 +210,40 @@ final class Lsp4jProtocolMessageMapperTest { assertEquals(SymbolKind.Field, mapped.get(1).getChildren().getFirst().getKind()); assertTrue(mapper.mapDocumentSymbols(BaselineDocumentSymbols.empty()).isEmpty()); } + + @Test + void workspaceSymbolsMapToFlatSymbolInformation() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var mapped = mapper.mapWorkspaceSymbols(new p.studio.lsp.messages.BaselineWorkspaceSymbols(List.of( + new p.studio.lsp.messages.BaselineWorkspaceSymbol( + "blend", + BaselineDocumentSymbolKind.METHOD, + "(dx: int, dy: int) -> int", + "Vec", + "file:///tmp/demo.pbs", + 3, + 2, + 3, + 7), + new p.studio.lsp.messages.BaselineWorkspaceSymbol( + "Idle", + BaselineDocumentSymbolKind.ENUM, + "0", + "Mode", + "file:///tmp/demo.pbs", + 0, + 19, + 0, + 23)))); + + assertEquals(2, mapped.size()); + assertEquals("blend", mapped.getFirst().getName()); + assertEquals("Vec", mapped.getFirst().getContainerName()); + assertEquals(SymbolKind.Method, mapped.getFirst().getKind()); + assertEquals("file:///tmp/demo.pbs", mapped.getFirst().getLocation().getUri()); + assertEquals(2, mapped.getFirst().getLocation().getRange().getStart().getCharacter()); + assertEquals("Idle", mapped.get(1).getName()); + assertEquals(SymbolKind.EnumMember, mapped.get(1).getKind()); + assertTrue(mapper.mapWorkspaceSymbols(p.studio.lsp.messages.BaselineWorkspaceSymbols.empty()).isEmpty()); + } }