From f6774ef5685a43a1501601e256ec6c5b24c0af61 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Tue, 5 May 2026 15:57:27 +0100 Subject: [PATCH] implements PLN-0067 --- discussion/index.ndjson | 2 +- ...ompiler-backed-dumb-lsp-server-baseline.md | 14 +- .../v1/compiler/BaselineDocumentAnalysis.java | 12 ++ .../v1/compiler/BaselineDocumentIssue.java | 29 ++++ .../studio/lsp/v1/compiler/BaselineHover.java | 15 ++ .../v1/compiler/BaselineIssueSeverity.java | 7 + .../compiler/BaselineServerDescription.java | 24 ++++ .../CompilerLanguageServiceBridge.java | 62 +++++++- .../v1/compiler/LanguageServiceBridge.java | 13 ++ .../v1/protocol/PrometeuLanguageServer.java | 35 ++--- .../protocol/PrometeuTextDocumentService.java | 69 ++++----- .../mapping/Lsp4jProtocolMessageMapper.java | 78 ++++++++++ .../mapping/ProtocolMessageMapper.java | 21 +++ .../mapping/ProtocolServerProfile.java | 11 -- .../mapping/ServerCapabilitiesMapper.java | 27 ---- .../CompilerLanguageServiceBridgeTest.java | 29 ++++ .../protocol/PrometeuLanguageServerTest.java | 116 +++++++++++++++ .../PrometeuTextDocumentServiceTest.java | 136 ++++++++++++++++++ 18 files changed, 597 insertions(+), 103 deletions(-) create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentAnalysis.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentIssue.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineHover.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineIssueSeverity.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineServerDescription.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/LanguageServiceBridge.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/Lsp4jProtocolMessageMapper.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolMessageMapper.java delete mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolServerProfile.java delete mode 100644 prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ServerCapabilitiesMapper.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridgeTest.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuLanguageServerTest.java create mode 100644 prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentServiceTest.java diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 105c34f9..c862806e 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":33,"AGD":36,"DEC":33,"PLN":68,"LSN":47,"CLSN":1}} -{"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0065","file":"PLN-0065-lsp-boundary-and-module-scaffolding.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0066","file":"PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0067","file":"PLN-0067-compiler-backed-dumb-lsp-server-baseline.md","status":"open","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0065","file":"PLN-0065-lsp-boundary-and-module-scaffolding.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0066","file":"PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0067","file":"PLN-0067-compiler-backed-dumb-lsp-server-baseline.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]}],"lessons":[]} {"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} {"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} diff --git a/discussion/workflow/plans/PLN-0067-compiler-backed-dumb-lsp-server-baseline.md b/discussion/workflow/plans/PLN-0067-compiler-backed-dumb-lsp-server-baseline.md index d320f1f8..b9e652ce 100644 --- a/discussion/workflow/plans/PLN-0067-compiler-backed-dumb-lsp-server-baseline.md +++ b/discussion/workflow/plans/PLN-0067-compiler-backed-dumb-lsp-server-baseline.md @@ -2,9 +2,9 @@ id: PLN-0067 ticket: studio-new-lsp-api-and-v1-boundary title: Compiler-Backed Dumb LSP Server Baseline -status: open +status: done created: 2026-05-05 -completed: +completed: 2026-05-05 tags: [studio, lsp, compiler, baseline, protocol, vscode] --- @@ -81,11 +81,11 @@ Establish a maintainable, compiler-backed baseline inside `lsp-v1` that keeps th ## Acceptance Criteria -- [ ] `lsp-v1` talks to `compiler` through explicit bridge services. -- [ ] The baseline server behavior remains intentionally simple but structurally clean. -- [ ] At least one minimal end-to-end capability slice proves the final layering. -- [ ] The extension can still connect over TCP without protocol regressions. -- [ ] Temporary semantic limits are explicit in tests and code structure. +- [x] `lsp-v1` talks to `compiler` through explicit bridge services. +- [x] The baseline server behavior remains intentionally simple but structurally clean. +- [x] At least one minimal end-to-end capability slice proves the final layering. +- [x] The extension can still connect over TCP without protocol regressions. +- [x] Temporary semantic limits are explicit in tests and code structure. ## Dependencies diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentAnalysis.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentAnalysis.java new file mode 100644 index 00000000..edb457fe --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentAnalysis.java @@ -0,0 +1,12 @@ +package p.studio.lsp.v1.compiler; + +import java.util.List; +import java.util.Objects; + +public record BaselineDocumentAnalysis( + List issues) { + + public BaselineDocumentAnalysis { + issues = List.copyOf(Objects.requireNonNull(issues, "issues")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentIssue.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentIssue.java new file mode 100644 index 00000000..6b19dc8d --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineDocumentIssue.java @@ -0,0 +1,29 @@ +package p.studio.lsp.v1.compiler; + +import java.util.Objects; + +public record BaselineDocumentIssue( + int startLine, + int startCharacter, + int endLine, + int endCharacter, + BaselineIssueSeverity severity, + String source, + String message) { + + public BaselineDocumentIssue { + severity = Objects.requireNonNull(severity, "severity"); + source = requireText(source, "source"); + message = requireText(message, "message"); + } + + 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/v1/compiler/BaselineHover.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineHover.java new file mode 100644 index 00000000..46efc4d2 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineHover.java @@ -0,0 +1,15 @@ +package p.studio.lsp.v1.compiler; + +import java.util.Objects; + +public record BaselineHover( + String markdown) { + + public BaselineHover { + final String candidate = Objects.requireNonNull(markdown, "markdown").trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException("markdown must not be blank"); + } + markdown = candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineIssueSeverity.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineIssueSeverity.java new file mode 100644 index 00000000..f6f7caa8 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineIssueSeverity.java @@ -0,0 +1,7 @@ +package p.studio.lsp.v1.compiler; + +public enum BaselineIssueSeverity { + INFORMATION, + WARNING, + ERROR +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineServerDescription.java new file mode 100644 index 00000000..dab48834 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/BaselineServerDescription.java @@ -0,0 +1,24 @@ +package p.studio.lsp.v1.compiler; + +import java.util.Objects; + +public record BaselineServerDescription( + String name, + String version, + boolean hoverSupported) { + + public BaselineServerDescription { + name = requireText(name, "name"); + version = requireText(version, "version"); + } + + 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/v1/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridge.java index 6495745b..8021ec50 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridge.java @@ -1,10 +1,11 @@ package p.studio.lsp.v1.compiler; import p.studio.compiler.workspaces.BuilderPipelineService; +import p.studio.lsp.api.LspProjectContext; import java.util.Objects; -public final class CompilerLanguageServiceBridge { +public final class CompilerLanguageServiceBridge implements LanguageServiceBridge { private final BuilderPipelineService builderPipelineService; public CompilerLanguageServiceBridge() { @@ -18,4 +19,63 @@ public final class CompilerLanguageServiceBridge { public BuilderPipelineService builderPipelineService() { return builderPipelineService; } + + @Override + public BaselineServerDescription describeServer(final LspProjectContext project) { + Objects.requireNonNull(project, "project"); + return new BaselineServerDescription( + "Prometeu Studio LSP", + "0.1.0", + true); + } + + @Override + public BaselineDocumentAnalysis analyzeDocument( + final LspProjectContext project, + final String documentUri, + final String text) { + Objects.requireNonNull(project, "project"); + // Wave 1 keeps responses intentionally synthetic while hardening the bridge boundary. + final String message = "Compiler-backed baseline via " + builderPipelineService.getClass().getSimpleName(); + return new BaselineDocumentAnalysis(java.util.List.of(new BaselineDocumentIssue( + 0, + 0, + 0, + Math.max(1, firstLineLength(text)), + BaselineIssueSeverity.INFORMATION, + "Prometeu Studio", + message))); + } + + @Override + public BaselineHover hover( + final LspProjectContext project, + final String documentUri, + final int line, + final int character) { + Objects.requireNonNull(project, "project"); + return new BaselineHover(""" + **Prometeu Studio LSP** + + Compiler-backed baseline. + + Semantic enrichment will be layered later. + """); + } + + @Override + public String onSave( + final LspProjectContext project, + final String documentUri) { + Objects.requireNonNull(project, "project"); + return "Prometeu Studio saw save: " + documentUri; + } + + private int firstLineLength(final String text) { + if (text == null || text.isEmpty()) { + return 1; + } + final int newline = text.indexOf('\n'); + return newline < 0 ? text.length() : newline; + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/LanguageServiceBridge.java new file mode 100644 index 00000000..689d6fd2 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/compiler/LanguageServiceBridge.java @@ -0,0 +1,13 @@ +package p.studio.lsp.v1.compiler; + +import p.studio.lsp.api.LspProjectContext; + +public interface LanguageServiceBridge { + BaselineServerDescription describeServer(LspProjectContext project); + + BaselineDocumentAnalysis analyzeDocument(LspProjectContext project, String documentUri, String text); + + BaselineHover hover(LspProjectContext project, String documentUri, int line, int character); + + String onSave(LspProjectContext project, String documentUri); +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuLanguageServer.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuLanguageServer.java index d10f225b..3cf5b036 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuLanguageServer.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuLanguageServer.java @@ -8,16 +8,17 @@ import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.WorkspaceService; import p.studio.lsp.api.LspProjectContext; import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge; -import p.studio.lsp.v1.protocol.mapping.ProtocolServerProfile; -import p.studio.lsp.v1.protocol.mapping.ServerCapabilitiesMapper; +import p.studio.lsp.v1.compiler.LanguageServiceBridge; +import p.studio.lsp.v1.protocol.mapping.Lsp4jProtocolMessageMapper; +import p.studio.lsp.v1.protocol.mapping.ProtocolMessageMapper; import java.util.Objects; import java.util.concurrent.CompletableFuture; public final class PrometeuLanguageServer implements LanguageServer, LanguageClientAware { private final LspProjectContext project; - private final CompilerLanguageServiceBridge compilerBridge; - private final ServerCapabilitiesMapper capabilitiesMapper; + private final LanguageServiceBridge languageServiceBridge; + private final ProtocolMessageMapper protocolMessageMapper; private final TextDocumentService textDocumentService; private final WorkspaceService workspaceService; private volatile LanguageClient client; @@ -28,21 +29,23 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli final CompilerLanguageServiceBridge compilerBridge) { this(project, compilerBridge, - new ServerCapabilitiesMapper(), - new PrometeuTextDocumentService(), + new Lsp4jProtocolMessageMapper(), + null, new PrometeuWorkspaceService()); } PrometeuLanguageServer( final LspProjectContext project, - final CompilerLanguageServiceBridge compilerBridge, - final ServerCapabilitiesMapper capabilitiesMapper, + final LanguageServiceBridge languageServiceBridge, + final ProtocolMessageMapper protocolMessageMapper, final TextDocumentService textDocumentService, final WorkspaceService workspaceService) { this.project = Objects.requireNonNull(project, "project"); - this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge"); - this.capabilitiesMapper = Objects.requireNonNull(capabilitiesMapper, "capabilitiesMapper"); - this.textDocumentService = Objects.requireNonNull(textDocumentService, "textDocumentService"); + this.languageServiceBridge = Objects.requireNonNull(languageServiceBridge, "languageServiceBridge"); + this.protocolMessageMapper = Objects.requireNonNull(protocolMessageMapper, "protocolMessageMapper"); + this.textDocumentService = textDocumentService != null + ? textDocumentService + : new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper); this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService"); } @@ -56,8 +59,8 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli @Override public CompletableFuture initialize(final InitializeParams params) { - final ProtocolServerProfile profile = ProtocolServerProfile.defaults(); - return CompletableFuture.completedFuture(capabilitiesMapper.map(profile)); + return CompletableFuture.completedFuture(protocolMessageMapper.mapInitializeResult( + languageServiceBridge.describeServer(project))); } @Override @@ -66,8 +69,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli if (currentClient == null) { return; } - currentClient.logMessage(new MessageParams( - MessageType.Info, + currentClient.logMessage(protocolMessageMapper.mapInfoMessage( "Prometeu Studio LSP connected for project " + project.projectKey())); } @@ -82,8 +84,7 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli if (!shutdownRequested) { final LanguageClient currentClient = client; if (currentClient != null) { - currentClient.logMessage(new MessageParams( - MessageType.Warning, + currentClient.logMessage(new MessageParams(MessageType.Warning, "Prometeu Studio LSP received exit before shutdown")); } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentService.java index 952e2772..c81edb55 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentService.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentService.java @@ -3,20 +3,35 @@ package p.studio.lsp.v1.protocol; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.TextDocumentService; +import p.studio.lsp.api.LspProjectContext; +import p.studio.lsp.v1.compiler.LanguageServiceBridge; +import p.studio.lsp.v1.protocol.mapping.ProtocolMessageMapper; -import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; public final class PrometeuTextDocumentService implements TextDocumentService { + private final LspProjectContext project; + private final LanguageServiceBridge languageServiceBridge; + private final ProtocolMessageMapper protocolMessageMapper; private volatile LanguageClient client; + public PrometeuTextDocumentService( + final LspProjectContext project, + final LanguageServiceBridge languageServiceBridge, + final ProtocolMessageMapper protocolMessageMapper) { + this.project = Objects.requireNonNull(project, "project"); + this.languageServiceBridge = Objects.requireNonNull(languageServiceBridge, "languageServiceBridge"); + this.protocolMessageMapper = Objects.requireNonNull(protocolMessageMapper, "protocolMessageMapper"); + } + void connect(final LanguageClient client) { this.client = client; } @Override public void didOpen(final DidOpenTextDocumentParams params) { - publishHelloDiagnostic(params.getTextDocument().getUri(), params.getTextDocument().getText()); + publishAnalysis(params.getTextDocument().getUri(), params.getTextDocument().getText()); } @Override @@ -24,7 +39,7 @@ public final class PrometeuTextDocumentService implements TextDocumentService { final String text = params.getContentChanges().isEmpty() ? "" : params.getContentChanges().getFirst().getText(); - publishHelloDiagnostic(params.getTextDocument().getUri(), text); + publishAnalysis(params.getTextDocument().getUri(), text); } @Override @@ -33,7 +48,7 @@ public final class PrometeuTextDocumentService implements TextDocumentService { if (currentClient == null) { return; } - currentClient.publishDiagnostics(new PublishDiagnosticsParams(params.getTextDocument().getUri(), List.of())); + currentClient.publishDiagnostics(protocolMessageMapper.emptyDiagnostics(params.getTextDocument().getUri())); } @Override @@ -42,52 +57,28 @@ public final class PrometeuTextDocumentService implements TextDocumentService { if (currentClient == null) { return; } - currentClient.logMessage(new MessageParams( - MessageType.Info, - "Prometeu Studio saw save: " + params.getTextDocument().getUri())); + currentClient.logMessage(protocolMessageMapper.mapInfoMessage( + languageServiceBridge.onSave(project, params.getTextDocument().getUri()))); } @Override public CompletableFuture hover(final HoverParams params) { - final MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.MARKDOWN); - content.setValue(""" - **Prometeu Studio LSP** - - Structured baseline server. - - Semantic enrichment will be layered later. - """); - - final Hover hover = new Hover(); - hover.setContents(content); - return CompletableFuture.completedFuture(hover); + return CompletableFuture.completedFuture(protocolMessageMapper.mapHover(languageServiceBridge.hover( + project, + params.getTextDocument().getUri(), + params.getPosition().getLine(), + params.getPosition().getCharacter()))); } - private void publishHelloDiagnostic( + private void publishAnalysis( final String uri, final String text) { final LanguageClient currentClient = client; if (currentClient == null) { return; } - - final Diagnostic diagnostic = new Diagnostic(); - diagnostic.setRange(new Range( - new Position(0, 0), - new Position(0, Math.max(1, firstLineLength(text))))); - diagnostic.setSeverity(DiagnosticSeverity.Information); - diagnostic.setSource("Prometeu Studio"); - diagnostic.setMessage("Structured baseline LSP response"); - - currentClient.publishDiagnostics(new PublishDiagnosticsParams(uri, List.of(diagnostic))); - } - - private int firstLineLength(final String text) { - if (text == null || text.isEmpty()) { - return 1; - } - final int newline = text.indexOf('\n'); - return newline < 0 ? text.length() : newline; + currentClient.publishDiagnostics(protocolMessageMapper.mapDiagnostics( + uri, + languageServiceBridge.analyzeDocument(project, uri, text))); } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/Lsp4jProtocolMessageMapper.java new file mode 100644 index 00000000..a6cee146 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -0,0 +1,78 @@ +package p.studio.lsp.v1.protocol.mapping; + +import org.eclipse.lsp4j.*; +import p.studio.lsp.v1.compiler.*; + +import java.util.List; + +public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { + @Override + public InitializeResult mapInitializeResult(final BaselineServerDescription description) { + final ServerCapabilities capabilities = new ServerCapabilities(); + + final TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions(); + syncOptions.setOpenClose(true); + syncOptions.setChange(TextDocumentSyncKind.Full); + + final SaveOptions saveOptions = new SaveOptions(); + saveOptions.setIncludeText(true); + syncOptions.setSave(saveOptions); + + capabilities.setTextDocumentSync(syncOptions); + capabilities.setHoverProvider(description.hoverSupported()); + + final InitializeResult result = new InitializeResult(capabilities); + final ServerInfo serverInfo = new ServerInfo(); + serverInfo.setName(description.name()); + serverInfo.setVersion(description.version()); + result.setServerInfo(serverInfo); + return result; + } + + @Override + public PublishDiagnosticsParams mapDiagnostics( + final String uri, + final BaselineDocumentAnalysis analysis) { + return new PublishDiagnosticsParams(uri, analysis.issues().stream().map(this::mapDiagnostic).toList()); + } + + @Override + public PublishDiagnosticsParams emptyDiagnostics(final String uri) { + return new PublishDiagnosticsParams(uri, List.of()); + } + + @Override + public Hover mapHover(final BaselineHover hover) { + final MarkupContent content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(hover.markdown()); + + final Hover result = new Hover(); + result.setContents(content); + return result; + } + + @Override + public MessageParams mapInfoMessage(final String message) { + return new MessageParams(MessageType.Info, message); + } + + private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) { + final Diagnostic diagnostic = new Diagnostic(); + diagnostic.setRange(new Range( + new Position(issue.startLine(), issue.startCharacter()), + new Position(issue.endLine(), issue.endCharacter()))); + diagnostic.setSeverity(mapSeverity(issue.severity())); + diagnostic.setSource(issue.source()); + diagnostic.setMessage(issue.message()); + return diagnostic; + } + + private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) { + return switch (severity) { + case INFORMATION -> DiagnosticSeverity.Information; + case WARNING -> DiagnosticSeverity.Warning; + case ERROR -> DiagnosticSeverity.Error; + }; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolMessageMapper.java new file mode 100644 index 00000000..d1f9922d --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolMessageMapper.java @@ -0,0 +1,21 @@ +package p.studio.lsp.v1.protocol.mapping; + +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import p.studio.lsp.v1.compiler.BaselineDocumentAnalysis; +import p.studio.lsp.v1.compiler.BaselineHover; +import p.studio.lsp.v1.compiler.BaselineServerDescription; + +public interface ProtocolMessageMapper { + InitializeResult mapInitializeResult(BaselineServerDescription description); + + PublishDiagnosticsParams mapDiagnostics(String uri, BaselineDocumentAnalysis analysis); + + PublishDiagnosticsParams emptyDiagnostics(String uri); + + Hover mapHover(BaselineHover hover); + + MessageParams mapInfoMessage(String message); +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolServerProfile.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolServerProfile.java deleted file mode 100644 index 75b4bc8a..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ProtocolServerProfile.java +++ /dev/null @@ -1,11 +0,0 @@ -package p.studio.lsp.v1.protocol.mapping; - -public record ProtocolServerProfile( - String name, - String version, - boolean hoverSupported) { - - public static ProtocolServerProfile defaults() { - return new ProtocolServerProfile("Prometeu Studio LSP", "0.1.0", true); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ServerCapabilitiesMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ServerCapabilitiesMapper.java deleted file mode 100644 index 5f5c4a0c..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/v1/protocol/mapping/ServerCapabilitiesMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package p.studio.lsp.v1.protocol.mapping; - -import org.eclipse.lsp4j.*; - -public final class ServerCapabilitiesMapper { - public InitializeResult map(final ProtocolServerProfile profile) { - final ServerCapabilities capabilities = new ServerCapabilities(); - - final TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions(); - syncOptions.setOpenClose(true); - syncOptions.setChange(TextDocumentSyncKind.Full); - - final SaveOptions saveOptions = new SaveOptions(); - saveOptions.setIncludeText(true); - syncOptions.setSave(saveOptions); - - capabilities.setTextDocumentSync(syncOptions); - capabilities.setHoverProvider(profile.hoverSupported()); - - final InitializeResult result = new InitializeResult(capabilities); - final ServerInfo serverInfo = new ServerInfo(); - serverInfo.setName(profile.name()); - serverInfo.setVersion(profile.version()); - result.setServerInfo(serverInfo); - return result; - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridgeTest.java new file mode 100644 index 00000000..1db5da43 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/compiler/CompilerLanguageServiceBridgeTest.java @@ -0,0 +1,29 @@ +package p.studio.lsp.v1.compiler; + +import org.junit.jupiter.api.Test; +import p.studio.lsp.api.LspProjectContext; + +import java.nio.file.Path; + +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 CompilerLanguageServiceBridgeTest { + + @Test + void bridgeProvidesStableBaselineResponses() { + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final LspProjectContext project = new LspProjectContext("demo", "pbs", Path.of(".")); + + final BaselineServerDescription server = bridge.describeServer(project); + final BaselineDocumentAnalysis analysis = bridge.analyzeDocument(project, "file:///demo.pbs", "let x = 1"); + final BaselineHover hover = bridge.hover(project, "file:///demo.pbs", 0, 0); + + assertEquals("Prometeu Studio LSP", server.name()); + assertTrue(server.hoverSupported()); + assertFalse(analysis.issues().isEmpty()); + assertTrue(analysis.issues().getFirst().message().contains("BuilderPipelineService")); + assertTrue(hover.markdown().contains("Compiler-backed baseline")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuLanguageServerTest.java new file mode 100644 index 00000000..819d4c5e --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuLanguageServerTest.java @@ -0,0 +1,116 @@ +package p.studio.lsp.v1.protocol; + +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.junit.jupiter.api.Test; +import p.studio.lsp.api.LspProjectContext; +import p.studio.lsp.v1.compiler.*; +import p.studio.lsp.v1.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; + +class PrometeuLanguageServerTest { + + @Test + void initializeDelegatesThroughBridgeAndMapper() { + final RecordingBridge bridge = new RecordingBridge(); + final RecordingMapper mapper = new RecordingMapper(); + final InitializeResult expected = new InitializeResult(); + mapper.initializeResult = expected; + + final PrometeuLanguageServer server = new PrometeuLanguageServer( + new LspProjectContext("demo", "pbs", Path.of(".")), + bridge, + mapper, + new NoopTextDocumentService(), + new PrometeuWorkspaceService()); + + final InitializeResult result = server.initialize(new InitializeParams()).join(); + + assertSame(expected, result); + assertEquals(1, bridge.describeServerCalls); + assertSame(bridge.serverDescription, mapper.lastServerDescription); + } + + private static final class RecordingBridge implements LanguageServiceBridge { + private int describeServerCalls; + private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true); + + @Override + public BaselineServerDescription describeServer(final LspProjectContext project) { + describeServerCalls++; + return serverDescription; + } + + @Override + public BaselineDocumentAnalysis analyzeDocument(final LspProjectContext project, final String documentUri, final String text) { + return new BaselineDocumentAnalysis(List.of()); + } + + @Override + public BaselineHover hover(final LspProjectContext project, final String documentUri, final int line, final int character) { + return new BaselineHover("hover"); + } + + @Override + public String onSave(final LspProjectContext project, final String documentUri) { + return "saved"; + } + } + + private static final class RecordingMapper implements ProtocolMessageMapper { + private InitializeResult initializeResult; + private BaselineServerDescription lastServerDescription; + + @Override + public InitializeResult mapInitializeResult(final BaselineServerDescription description) { + lastServerDescription = description; + return initializeResult; + } + + @Override + public PublishDiagnosticsParams mapDiagnostics(final String uri, final BaselineDocumentAnalysis analysis) { + throw new UnsupportedOperationException(); + } + + @Override + public PublishDiagnosticsParams emptyDiagnostics(final String uri) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.Hover mapHover(final BaselineHover hover) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) { + return new org.eclipse.lsp4j.MessageParams(); + } + } + + private static final class NoopTextDocumentService implements TextDocumentService { + @Override + public void didOpen(final org.eclipse.lsp4j.DidOpenTextDocumentParams params) { + } + + @Override + public void didChange(final org.eclipse.lsp4j.DidChangeTextDocumentParams params) { + } + + @Override + public void didClose(final org.eclipse.lsp4j.DidCloseTextDocumentParams params) { + } + + @Override + public void didSave(final org.eclipse.lsp4j.DidSaveTextDocumentParams params) { + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentServiceTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentServiceTest.java new file mode 100644 index 00000000..f7b442d2 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/v1/protocol/PrometeuTextDocumentServiceTest.java @@ -0,0 +1,136 @@ +package p.studio.lsp.v1.protocol; + +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.services.LanguageClient; +import org.junit.jupiter.api.Test; +import p.studio.lsp.api.LspProjectContext; +import p.studio.lsp.v1.compiler.*; +import p.studio.lsp.v1.protocol.mapping.ProtocolMessageMapper; + +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class PrometeuTextDocumentServiceTest { + + @Test + void documentEventsDelegateToBridgeAndMapper() { + final RecordingBridge bridge = new RecordingBridge(); + final RecordingMapper mapper = new RecordingMapper(); + final RecordingClient client = new RecordingClient(); + final PrometeuTextDocumentService service = new PrometeuTextDocumentService( + new LspProjectContext("demo", "pbs", Path.of(".")), + bridge, + mapper); + service.connect(client); + + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem("file:///demo.pbs", "pbs", 1, "abc"))); + service.didSave(new DidSaveTextDocumentParams(new TextDocumentIdentifier("file:///demo.pbs"), "abc")); + service.hover(new HoverParams(new TextDocumentIdentifier("file:///demo.pbs"), new Position(0, 0))).join(); + + assertEquals(1, bridge.analyzeCalls); + assertEquals(1, bridge.saveCalls); + assertEquals(1, bridge.hoverCalls); + assertSame(bridge.lastAnalysis, mapper.lastAnalysis); + assertSame(bridge.lastHover, mapper.lastHover); + assertSame(mapper.diagnostics, client.lastDiagnostics); + assertSame(mapper.infoMessage, client.lastLogMessage); + } + + private static final class RecordingBridge implements LanguageServiceBridge { + private int analyzeCalls; + private int saveCalls; + private int hoverCalls; + private final BaselineDocumentAnalysis lastAnalysis = new BaselineDocumentAnalysis(List.of( + new BaselineDocumentIssue(0, 0, 0, 1, BaselineIssueSeverity.INFORMATION, "src", "msg"))); + private final BaselineHover lastHover = new BaselineHover("hover"); + + @Override + public BaselineServerDescription describeServer(final LspProjectContext project) { + return new BaselineServerDescription("demo", "1", true); + } + + @Override + public BaselineDocumentAnalysis analyzeDocument(final LspProjectContext project, final String documentUri, final String text) { + analyzeCalls++; + return lastAnalysis; + } + + @Override + public BaselineHover hover(final LspProjectContext project, final String documentUri, final int line, final int character) { + hoverCalls++; + return lastHover; + } + + @Override + public String onSave(final LspProjectContext project, final String documentUri) { + saveCalls++; + return "saved"; + } + } + + private static final class RecordingMapper implements ProtocolMessageMapper { + private final PublishDiagnosticsParams diagnostics = new PublishDiagnosticsParams("file:///demo.pbs", List.of()); + private final MessageParams infoMessage = new MessageParams(MessageType.Info, "saved"); + private BaselineDocumentAnalysis lastAnalysis; + private BaselineHover lastHover; + + @Override + public InitializeResult mapInitializeResult(final BaselineServerDescription description) { + throw new UnsupportedOperationException(); + } + + @Override + public PublishDiagnosticsParams mapDiagnostics(final String uri, final BaselineDocumentAnalysis analysis) { + lastAnalysis = analysis; + return diagnostics; + } + + @Override + public PublishDiagnosticsParams emptyDiagnostics(final String uri) { + return new PublishDiagnosticsParams(uri, List.of()); + } + + @Override + public Hover mapHover(final BaselineHover hover) { + lastHover = hover; + return new Hover(); + } + + @Override + public MessageParams mapInfoMessage(final String message) { + return infoMessage; + } + } + + private static final class RecordingClient implements LanguageClient { + private PublishDiagnosticsParams lastDiagnostics; + private MessageParams lastLogMessage; + + @Override + public void telemetryEvent(final Object object) { + } + + @Override + public void publishDiagnostics(final PublishDiagnosticsParams diagnostics) { + lastDiagnostics = diagnostics; + } + + @Override + public void showMessage(final MessageParams messageParams) { + } + + @Override + public CompletableFuture showMessageRequest(final ShowMessageRequestParams requestParams) { + return CompletableFuture.completedFuture(null); + } + + @Override + public void logMessage(final MessageParams message) { + lastLogMessage = message; + } + } +}