dev/prometeu-lsp #8

Merged
bquarkz merged 37 commits from dev/prometeu-lsp into master 2026-05-08 09:54:17 +00:00
8 changed files with 548 additions and 422 deletions
Showing only changes of commit ba88dee8cb - Show all commits

View File

@ -1,29 +0,0 @@
package p.studio.lsp.v1;
import p.studio.lsp.api.LspProjectContext;
import p.studio.lsp.api.LspServerBootRequest;
import p.studio.lsp.v1.bootstrap.LspV1ServerLifecycle;
import p.studio.lsp.v1.compiler.CompilerLanguageServiceBridge;
import p.studio.lsp.v1.host.TcpLspServerHostFactory;
import java.nio.file.Path;
public final class PrometeuStudioLspMain {
private PrometeuStudioLspMain() {
}
public static void main(final String[] args) throws Exception {
final var lifecycle = new LspV1ServerLifecycle(
new TcpLspServerHostFactory(),
new CompilerLanguageServiceBridge());
final var request = LspServerBootRequest.defaults(new LspProjectContext(
"standalone-mock",
"pbs",
Path.of(".")));
final var handle = lifecycle.bootServer(request);
System.out.println("Prometeu Studio LSP listening on " + handle.endpoint().host() + ":" + handle.endpoint().port());
Thread.currentThread().join();
}
}

View File

@ -0,0 +1,79 @@
package p.studio.lsp.v1.protocol;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import p.studio.lsp.api.LspProjectContext;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
final class ProjectConnectionValidator {
private ProjectConnectionValidator() {
}
static void validateProjectBinding(
final LspProjectContext project,
final InitializeParams params) {
final LspProjectContext safeProject = Objects.requireNonNull(project, "project");
final InitializeParams safeParams = Objects.requireNonNull(params, "params");
final Path expectedRoot = safeProject.projectRoot().toAbsolutePath().normalize();
final List<Path> clientRoots = extractClientRoots(safeParams);
if (clientRoots.isEmpty()) {
throw mismatch(safeProject, "client did not declare rootUri or workspaceFolders");
}
for (final Path clientRoot : clientRoots) {
if (expectedRoot.equals(clientRoot)) {
return;
}
}
throw mismatch(safeProject, "client roots " + clientRoots + " do not match " + expectedRoot);
}
private static List<Path> extractClientRoots(final InitializeParams params) {
final List<Path> roots = new ArrayList<>();
if (params.getRootUri() != null && !params.getRootUri().isBlank()) {
roots.add(parsePath(params.getRootUri()));
}
if (params.getWorkspaceFolders() != null) {
for (final WorkspaceFolder workspaceFolder : params.getWorkspaceFolders()) {
if (workspaceFolder == null || workspaceFolder.getUri() == null || workspaceFolder.getUri().isBlank()) {
continue;
}
roots.add(parsePath(workspaceFolder.getUri()));
}
}
return List.copyOf(roots);
}
private static Path parsePath(final String rawUri) {
try {
return Path.of(URI.create(rawUri)).toAbsolutePath().normalize();
} catch (RuntimeException exception) {
throw new ResponseErrorException(new ResponseError(
ResponseErrorCode.InvalidParams,
"invalid project root uri from client: " + rawUri,
null));
}
}
private static ResponseErrorException mismatch(
final LspProjectContext project,
final String detail) {
return new ResponseErrorException(new ResponseError(
ResponseErrorCode.InvalidParams,
"Prometeu Studio LSP is bound to project root "
+ project.projectRoot()
+ " and rejected this client: "
+ detail,
null));
}
}

View File

@ -59,6 +59,13 @@ public final class PrometeuLanguageServer implements LanguageServer, LanguageCli
@Override @Override
public CompletableFuture<InitializeResult> initialize(final InitializeParams params) { public CompletableFuture<InitializeResult> initialize(final InitializeParams params) {
try {
ProjectConnectionValidator.validateProjectBinding(project, params);
} catch (RuntimeException runtimeException) {
client.logMessage(protocolMessageMapper.mapInfoMessage(
"Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage()));
return CompletableFuture.failedFuture(runtimeException);
}
return CompletableFuture.completedFuture(protocolMessageMapper.mapInitializeResult( return CompletableFuture.completedFuture(protocolMessageMapper.mapInitializeResult(
languageServiceBridge.describeServer(project))); languageServiceBridge.describeServer(project)));
} }

View File

@ -57,6 +57,11 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return new MessageParams(MessageType.Info, message); return new MessageParams(MessageType.Info, message);
} }
@Override
public MessageParams mapErrorMessage(final String message) {
return new MessageParams(MessageType.Error, message);
}
private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) { private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) {
final Diagnostic diagnostic = new Diagnostic(); final Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range( diagnostic.setRange(new Range(
@ -68,6 +73,7 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper {
return diagnostic; return diagnostic;
} }
private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) { private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) {
return switch (severity) { return switch (severity) {
case INFORMATION -> DiagnosticSeverity.Information; case INFORMATION -> DiagnosticSeverity.Information;

View File

@ -18,4 +18,5 @@ public interface ProtocolMessageMapper {
Hover mapHover(BaselineHover hover); Hover mapHover(BaselineHover hover);
MessageParams mapInfoMessage(String message); MessageParams mapInfoMessage(String message);
MessageParams mapErrorMessage(String message);
} }

View File

@ -2,6 +2,8 @@ package p.studio.lsp.v1.protocol;
import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.services.TextDocumentService; import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService; import org.eclipse.lsp4j.services.WorkspaceService;
@ -12,9 +14,12 @@ import p.studio.lsp.v1.protocol.mapping.ProtocolMessageMapper;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletionException;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrometeuLanguageServerTest { class PrometeuLanguageServerTest {
@ -32,13 +37,60 @@ class PrometeuLanguageServerTest {
new NoopTextDocumentService(), new NoopTextDocumentService(),
new PrometeuWorkspaceService()); new PrometeuWorkspaceService());
final InitializeResult result = server.initialize(new InitializeParams()).join(); final InitializeParams params = new InitializeParams();
params.setRootUri(Path.of(".").toAbsolutePath().normalize().toUri().toString());
final InitializeResult result = server.initialize(params).join();
assertSame(expected, result); assertSame(expected, result);
assertEquals(1, bridge.describeServerCalls); assertEquals(1, bridge.describeServerCalls);
assertSame(bridge.serverDescription, mapper.lastServerDescription); assertSame(bridge.serverDescription, mapper.lastServerDescription);
} }
@Test
void initializeRejectsClientBoundToDifferentProjectRoot() {
final RecordingBridge bridge = new RecordingBridge();
final RecordingMapper mapper = new RecordingMapper();
final PrometeuLanguageServer server = new PrometeuLanguageServer(
new LspProjectContext("demo", "pbs", Path.of("/tmp/studio-project")),
bridge,
mapper,
new NoopTextDocumentService(),
new PrometeuWorkspaceService());
final InitializeParams params = new InitializeParams();
params.setRootUri(Path.of("/tmp/another-project").toUri().toString());
final CompletionException failure = org.junit.jupiter.api.Assertions.assertThrows(
CompletionException.class,
() -> server.initialize(params).join());
final ResponseErrorException responseError = assertInstanceOf(ResponseErrorException.class, failure.getCause());
assertTrue(responseError.getMessage().contains("/tmp/studio-project"));
assertEquals(0, bridge.describeServerCalls);
}
@Test
void initializeAcceptsMatchingWorkspaceFolder() {
final RecordingBridge bridge = new RecordingBridge();
final RecordingMapper mapper = new RecordingMapper();
mapper.initializeResult = new InitializeResult();
final Path projectRoot = Path.of("/tmp/studio-project").toAbsolutePath().normalize();
final PrometeuLanguageServer server = new PrometeuLanguageServer(
new LspProjectContext("demo", "pbs", projectRoot),
bridge,
mapper,
new NoopTextDocumentService(),
new PrometeuWorkspaceService());
final InitializeParams params = new InitializeParams();
params.setWorkspaceFolders(List.of(new WorkspaceFolder(projectRoot.toUri().toString(), "studio-project")));
server.initialize(params).join();
assertEquals(1, bridge.describeServerCalls);
}
private static final class RecordingBridge implements LanguageServiceBridge { private static final class RecordingBridge implements LanguageServiceBridge {
private int describeServerCalls; private int describeServerCalls;
private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true); private final BaselineServerDescription serverDescription = new BaselineServerDescription("demo", "1", true);
@ -94,6 +146,11 @@ class PrometeuLanguageServerTest {
public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) { public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) {
return new org.eclipse.lsp4j.MessageParams(); return new org.eclipse.lsp4j.MessageParams();
} }
@Override
public org.eclipse.lsp4j.MessageParams mapErrorMessage(final String message) {
return new org.eclipse.lsp4j.MessageParams();
}
} }
private static final class NoopTextDocumentService implements TextDocumentService { private static final class NoopTextDocumentService implements TextDocumentService {

View File

@ -104,6 +104,11 @@ class PrometeuTextDocumentServiceTest {
public MessageParams mapInfoMessage(final String message) { public MessageParams mapInfoMessage(final String message) {
return infoMessage; return infoMessage;
} }
@Override
public MessageParams mapErrorMessage(final String message) {
return infoMessage;
}
} }
private static final class RecordingClient implements LanguageClient { private static final class RecordingClient implements LanguageClient {

File diff suppressed because it is too large Load Diff