diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java index 8764d3f1df1..39d90226e67 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLLanguageServer.java @@ -38,6 +38,7 @@ import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.ColorProviderOptions; import org.eclipse.lsp4j.DefinitionOptions; +import org.eclipse.lsp4j.DiagnosticRegistrationOptions; import org.eclipse.lsp4j.DocumentFormattingOptions; import org.eclipse.lsp4j.DocumentLinkOptions; import org.eclipse.lsp4j.DocumentRangeFormattingOptions; @@ -130,6 +131,7 @@ public CompletableFuture initialize(InitializeParams params) { capabilities.setRenameProvider(getRenameProvider(params)); capabilities.setInlayHintProvider(getInlayHintProvider()); capabilities.setExecuteCommandProvider(getExecuteCommandProvider()); + capabilities.setDiagnosticProvider(getDiagnosticProvider()); var result = new InitializeResult(capabilities, serverInfo); @@ -337,6 +339,14 @@ private static InlayHintRegistrationOptions getInlayHintProvider() { return inlayHintOptions; } + private static DiagnosticRegistrationOptions getDiagnosticProvider() { + var diagnosticOptions = new DiagnosticRegistrationOptions(); + diagnosticOptions.setWorkDoneProgress(Boolean.FALSE); + diagnosticOptions.setInterFileDependencies(Boolean.TRUE); + diagnosticOptions.setWorkspaceDiagnostics(Boolean.FALSE); + return diagnosticOptions; + } + private ExecuteCommandOptions getExecuteCommandProvider() { var executeCommandOptions = new ExecuteCommandOptions(); executeCommandOptions.setCommands(commandProvider.getCommandIds()); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java index 223d3b4a6bb..f361f1aa21e 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java @@ -25,6 +25,7 @@ import com.github._1c_syntax.bsl.languageserver.configuration.diagnostics.ComputeTrigger; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; import com.github._1c_syntax.bsl.languageserver.context.ServerContext; +import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; import com.github._1c_syntax.bsl.languageserver.jsonrpc.DiagnosticParams; import com.github._1c_syntax.bsl.languageserver.jsonrpc.Diagnostics; import com.github._1c_syntax.bsl.languageserver.jsonrpc.ProtocolExtension; @@ -52,6 +53,7 @@ import org.eclipse.lsp4j.CallHierarchyOutgoingCall; import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams; import org.eclipse.lsp4j.CallHierarchyPrepareParams; +import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CodeLens; @@ -66,6 +68,8 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.DocumentColorParams; +import org.eclipse.lsp4j.DocumentDiagnosticParams; +import org.eclipse.lsp4j.DocumentDiagnosticReport; import org.eclipse.lsp4j.DocumentFormattingParams; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentLinkParams; @@ -85,15 +89,18 @@ import org.eclipse.lsp4j.PrepareRenameResult; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.RelatedFullDocumentDiagnosticReport; import org.eclipse.lsp4j.RenameParams; import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.SelectionRangeParams; import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentClientCapabilities; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.messages.Either3; import org.eclipse.lsp4j.services.TextDocumentService; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import org.springframework.stereotype.Component; @@ -132,8 +139,11 @@ public class BSLTextDocumentService implements TextDocumentService, ProtocolExte private final ColorProvider colorProvider; private final RenameProvider renameProvider; private final InlayHintProvider inlayHintProvider; + private final ClientCapabilitiesHolder clientCapabilitiesHolder; private final ExecutorService executorService = Executors.newCachedThreadPool(new CustomizableThreadFactory("text-document-service-")); + + private boolean clientSupportsPullDiagnostics; @PreDestroy private void onDestroy() { @@ -466,6 +476,21 @@ public CompletableFuture diagnostics(DiagnosticParams params) { }); } + @Override + public CompletableFuture diagnostic(DocumentDiagnosticParams params) { + var documentContext = context.getDocument(params.getTextDocument().getUri()); + if (documentContext == null) { + return CompletableFuture.completedFuture( + new DocumentDiagnosticReport(new RelatedFullDocumentDiagnosticReport(Collections.emptyList())) + ); + } + + return CompletableFuture.supplyAsync( + () -> diagnosticProvider.getDiagnostic(documentContext), + executorService + ); + } + @Override public CompletableFuture> prepareRename(PrepareRenameParams params) { var documentContext = context.getDocument(params.getTextDocument().getUri()); @@ -496,7 +521,25 @@ public void reset() { context.clear(); } + /** + * Обработчик события {@link LanguageServerInitializeRequestReceivedEvent}. + *

+ * Проверяет поддержку клиентом pull-модели диагностик. + * + * @param event Событие + */ + @EventListener + public void handleInitializeEvent(LanguageServerInitializeRequestReceivedEvent event) { + clientSupportsPullDiagnostics = clientCapabilitiesHolder.getCapabilities() + .map(ClientCapabilities::getTextDocument) + .map(TextDocumentClientCapabilities::getDiagnostic) + .isPresent(); + } + private void validate(DocumentContext documentContext) { + if (clientSupportsPullDiagnostics) { + return; + } diagnosticProvider.computeAndPublishDiagnostics(documentContext); } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProvider.java index cd46d1529d0..23f72a60dd0 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProvider.java @@ -21,11 +21,21 @@ */ package com.github._1c_syntax.bsl.languageserver.providers; +import com.github._1c_syntax.bsl.languageserver.ClientCapabilitiesHolder; import com.github._1c_syntax.bsl.languageserver.LanguageClientHolder; +import com.github._1c_syntax.bsl.languageserver.configuration.events.LanguageServerConfigurationChangedEvent; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticWorkspaceCapabilities; +import org.eclipse.lsp4j.DocumentDiagnosticReport; import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.RelatedFullDocumentDiagnosticReport; +import org.eclipse.lsp4j.WorkspaceClientCapabilities; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.Collections; @@ -33,12 +43,17 @@ import java.util.function.Supplier; /** - * Провайдер для публикации диагностических сообщений. + * Провайдер для диагностических сообщений. *

- * Отвечает за публикацию диагностик с использованием {@code textDocument/publishDiagnostics}. + * Отвечает за публикацию диагностик с использованием {@code textDocument/publishDiagnostics}, + * предоставление диагностик по запросу {@code textDocument/diagnostic} + * и уведомление об обновлении диагностик через {@code workspace/diagnostic/refresh}. * * @see PublishDiagnostics Notification specification + * @see Diagnostic Pull Request specification + * @see Diagnostic Refresh Request specification */ +@Slf4j @Component @RequiredArgsConstructor public final class DiagnosticProvider { @@ -46,6 +61,9 @@ public final class DiagnosticProvider { public static final String SOURCE = "bsl-language-server"; private final LanguageClientHolder clientHolder; + private final ClientCapabilitiesHolder clientCapabilitiesHolder; + + private boolean clientSupportsRefresh; /** * Вычислить и опубликовать диагностики для документа. @@ -56,6 +74,18 @@ public void computeAndPublishDiagnostics(DocumentContext documentContext) { publishDiagnostics(documentContext, documentContext::getDiagnostics); } + /** + * Получить диагностики для документа (pull-модель). + * + * @param documentContext Контекст документа + * @return Отчет с диагностиками + */ + public DocumentDiagnosticReport getDiagnostic(DocumentContext documentContext) { + var diagnostics = documentContext.getDiagnostics(); + var report = new RelatedFullDocumentDiagnosticReport(diagnostics); + return new DocumentDiagnosticReport(report); + } + /** * Опубликовать пустой список диагностик для документа. * @@ -65,6 +95,39 @@ public void publishEmptyDiagnosticList(DocumentContext documentContext) { publishDiagnostics(documentContext, Collections::emptyList); } + /** + * Обработчик события {@link LanguageServerInitializeRequestReceivedEvent}. + *

+ * Проверяет поддержку клиентом workspace/diagnostic/refresh. + * + * @param event Событие + */ + @EventListener + public void handleInitializeEvent(LanguageServerInitializeRequestReceivedEvent event) { + clientSupportsRefresh = clientCapabilitiesHolder.getCapabilities() + .map(ClientCapabilities::getWorkspace) + .map(WorkspaceClientCapabilities::getDiagnostics) + .map(DiagnosticWorkspaceCapabilities::getRefreshSupport) + .orElse(false); + } + + /** + * Обработчик события {@link LanguageServerConfigurationChangedEvent}. + *

+ * Отправляет клиенту запрос на обновление диагностик при изменении конфигурации. + * + * @param event Событие + */ + @EventListener + public void handleConfigurationChangedEvent(LanguageServerConfigurationChangedEvent event) { + if (clientSupportsRefresh) { + clientHolder.execIfConnected(languageClient -> { + LOGGER.debug("Requesting diagnostic refresh from client"); + languageClient.refreshDiagnostics(); + }); + } + } + private void publishDiagnostics(DocumentContext documentContext, Supplier> diagnostics) { clientHolder.execIfConnected(languageClient -> languageClient.publishDiagnostics( diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java index 17d3381bca7..649251629a5 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java @@ -29,6 +29,7 @@ import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentDiagnosticParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PrepareRenameParams; import org.eclipse.lsp4j.RenameParams; @@ -140,6 +141,35 @@ void testDiagnosticsKnownFileFilteredRange() throws ExecutionException, Interrup assertThat(diagnostics.getDiagnostics()).hasSize(2); } + @Test + void testStandardDiagnosticUnknownFile() throws ExecutionException, InterruptedException { + // when + var params = new DocumentDiagnosticParams(getTextDocumentIdentifier()); + var diagnosticReport = textDocumentService.diagnostic(params).get(); + + // then + assertThat(diagnosticReport).isNotNull(); + assertThat(diagnosticReport.getLeft()).isNotNull(); + assertThat(diagnosticReport.getLeft().getItems()).isEmpty(); + } + + @Test + void testStandardDiagnosticKnownFile() throws ExecutionException, InterruptedException, IOException { + // given + var textDocumentItem = getTextDocumentItem(); + var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem); + textDocumentService.didOpen(didOpenParams); + + // when + var params = new DocumentDiagnosticParams(getTextDocumentIdentifier()); + var diagnosticReport = textDocumentService.diagnostic(params).get(); + + // then + assertThat(diagnosticReport).isNotNull(); + assertThat(diagnosticReport.getLeft()).isNotNull(); + assertThat(diagnosticReport.getLeft().getItems()).isNotEmpty(); + } + @Test void testRename() throws ExecutionException, InterruptedException, IOException { var params = new RenameParams(); diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProviderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProviderTest.java index 2e17100eb9f..9d153f7309b 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProviderTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DiagnosticProviderTest.java @@ -21,16 +21,27 @@ */ package com.github._1c_syntax.bsl.languageserver.providers; +import com.github._1c_syntax.bsl.languageserver.LanguageClientHolder; +import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration; +import com.github._1c_syntax.bsl.languageserver.configuration.events.LanguageServerConfigurationChangedEvent; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticWorkspaceCapabilities; +import org.eclipse.lsp4j.DocumentDiagnosticReport; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.RelatedFullDocumentDiagnosticReport; +import org.eclipse.lsp4j.WorkspaceClientCapabilities; +import org.eclipse.lsp4j.services.LanguageServer; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import java.util.List; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.Mockito.mock; @SpringBootTest class DiagnosticProviderTest { @@ -38,18 +49,135 @@ class DiagnosticProviderTest { @Autowired private DiagnosticProvider diagnosticProvider; + @Autowired + private LanguageClientHolder languageClientHolder; + + @Test + void testComputeAndPublishDiagnostics() { + // given + final DocumentContext documentContext + = TestUtils.getDocumentContextFromFile("./src/test/resources/providers/diagnosticProvider.bsl"); + + // when-then + assertThatCode(() -> diagnosticProvider.computeAndPublishDiagnostics(documentContext)) + .doesNotThrowAnyException(); + } + + @Test + void testPublishEmptyDiagnosticList() { + // given + final DocumentContext documentContext + = TestUtils.getDocumentContextFromFile("./src/test/resources/providers/diagnosticProvider.bsl"); + + // when-then + assertThatCode(() -> diagnosticProvider.publishEmptyDiagnosticList(documentContext)) + .doesNotThrowAnyException(); + } + + @Test + void testGetDiagnostic() { + // given + final DocumentContext documentContext + = TestUtils.getDocumentContextFromFile("./src/test/resources/providers/diagnosticProvider.bsl"); + + // when + final DocumentDiagnosticReport report = diagnosticProvider.getDiagnostic(documentContext); + + // then + assertThat(report).isNotNull(); + assertThat(report.getLeft()).isNotNull(); + assertThat(report.getLeft()).isInstanceOf(RelatedFullDocumentDiagnosticReport.class); + + RelatedFullDocumentDiagnosticReport fullReport = report.getLeft(); + assertThat(fullReport.getItems()).isNotNull(); + assertThat(fullReport.getItems()).isNotEmpty(); + } + @Test - void testComputeDiagnostics() { + void testGetDiagnosticWithNoDiagnostics() { // given - // TODO: это тест на новый getDiagnostics, а не на DiagnosticProvider + final DocumentContext documentContext + = TestUtils.getDocumentContext(""); + // when + final DocumentDiagnosticReport report = diagnosticProvider.getDiagnostic(documentContext); + + // then + assertThat(report).isNotNull(); + assertThat(report.getLeft()).isNotNull(); + assertThat(report.getLeft()).isInstanceOf(RelatedFullDocumentDiagnosticReport.class); + + RelatedFullDocumentDiagnosticReport fullReport = report.getLeft(); + assertThat(fullReport.getItems()).isNotNull(); + assertThat(fullReport.getItems()).isEmpty(); + } + + @Test + void testGetDiagnosticReportStructure() { + // given final DocumentContext documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/providers/diagnosticProvider.bsl"); // when - final List diagnostics = documentContext.getDiagnostics(); + final DocumentDiagnosticReport report = diagnosticProvider.getDiagnostic(documentContext); // then - assertThat(diagnostics.size()).isPositive(); + RelatedFullDocumentDiagnosticReport fullReport = report.getLeft(); + assertThat(fullReport.getKind()).isEqualTo("full"); + assertThat(fullReport.getItems()).hasSizeGreaterThan(0); + + // Verify diagnostics have required fields + Diagnostic firstDiagnostic = fullReport.getItems().get(0); + assertThat(firstDiagnostic.getRange()).isNotNull(); + assertThat(firstDiagnostic.getMessage()).isNotNull(); + assertThat(firstDiagnostic.getSource()).isEqualTo(DiagnosticProvider.SOURCE); + } + + @Test + void testHandleInitializeEvent() { + // given + var languageServer = mock(LanguageServer.class); + var params = new InitializeParams(); + var capabilities = new ClientCapabilities(); + var workspace = new WorkspaceClientCapabilities(); + var diagnostics = new DiagnosticWorkspaceCapabilities(); + diagnostics.setRefreshSupport(true); + workspace.setDiagnostics(diagnostics); + capabilities.setWorkspace(workspace); + params.setCapabilities(capabilities); + + var event = new LanguageServerInitializeRequestReceivedEvent(languageServer, params); + + // when-then + assertThatCode(() -> diagnosticProvider.handleInitializeEvent(event)) + .doesNotThrowAnyException(); + } + + @Test + void testHandleInitializeEventWithoutDiagnosticsCapabilities() { + // given + var languageServer = mock(LanguageServer.class); + var params = new InitializeParams(); + var capabilities = new ClientCapabilities(); + var workspace = new WorkspaceClientCapabilities(); + capabilities.setWorkspace(workspace); + params.setCapabilities(capabilities); + + var event = new LanguageServerInitializeRequestReceivedEvent(languageServer, params); + + // when-then + assertThatCode(() -> diagnosticProvider.handleInitializeEvent(event)) + .doesNotThrowAnyException(); + } + + @Test + void testHandleConfigurationChangedEvent() { + // given + var configuration = mock(LanguageServerConfiguration.class); + var event = new LanguageServerConfigurationChangedEvent(configuration); + + // when-then + assertThatCode(() -> diagnosticProvider.handleConfigurationChangedEvent(event)) + .doesNotThrowAnyException(); } }