Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -130,6 +131,7 @@ public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
capabilities.setRenameProvider(getRenameProvider(params));
capabilities.setInlayHintProvider(getInlayHintProvider());
capabilities.setExecuteCommandProvider(getExecuteCommandProvider());
capabilities.setDiagnosticProvider(getDiagnosticProvider());

var result = new InitializeResult(capabilities, serverInfo);

Expand Down Expand Up @@ -337,6 +339,14 @@ private static InlayHintRegistrationOptions getInlayHintProvider() {
return inlayHintOptions;
}

private static DiagnosticRegistrationOptions getDiagnosticProvider() {
var diagnosticOptions = new DiagnosticRegistrationOptions();
diagnosticOptions.setWorkDoneProgress(Boolean.FALSE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot насколько сложно сделать поддержку workDoneProgress? Предложи вариант решения, но НЕ РЕАЛИЗОВЫВАЙ.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architecture proposal for workDoneProgress support

Complexity: Medium

Components needed:

  1. WorkDoneProgressHelper integration:

    • Already exists in ServerContext for populateContext
    • Extend for diagnostic computation
  2. DiagnosticProvider changes:

    • Accept WorkDoneProgressParams from request
    • Create progress reporter before computing diagnostics
    • Report progress during diagnostic computation (per-diagnostic or batched)
  3. Implementation approach:

    if (params.getWorkDoneToken() != null) {
      var progress = workDoneProgressHelper.createProgress(...);
      progress.begin("Computing diagnostics...");
      try {
        // compute diagnostics with progress updates
        progress.report(current, total);
      } finally {
        progress.end();
      }
    }
    

Challenges:

  • Diagnostics are usually fast (< 1s), progress may add overhead
  • Need to decide granularity (per diagnostic? per file? per batch?)
  • Integration with async execution in BSLTextDocumentService

diagnosticOptions.setInterFileDependencies(Boolean.TRUE);
diagnosticOptions.setWorkspaceDiagnostics(Boolean.FALSE);
return diagnosticOptions;
}

private ExecuteCommandOptions getExecuteCommandProvider() {
var executeCommandOptions = new ExecuteCommandOptions();
executeCommandOptions.setCommands(commandProvider.getCommandIds());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -466,6 +476,21 @@ public CompletableFuture<Diagnostics> diagnostics(DiagnosticParams params) {
});
}

@Override
public CompletableFuture<DocumentDiagnosticReport> 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<Either3<Range, PrepareRenameResult, PrepareRenameDefaultBehavior>> prepareRename(PrepareRenameParams params) {
var documentContext = context.getDocument(params.getTextDocument().getUri());
Expand Down Expand Up @@ -496,7 +521,25 @@ public void reset() {
context.clear();
}

/**
* Обработчик события {@link LanguageServerInitializeRequestReceivedEvent}.
* <p>
* Проверяет поддержку клиентом 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,49 @@
*/
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;
import java.util.List;
import java.util.function.Supplier;

/**
* Провайдер для публикации диагностических сообщений.
* Провайдер для диагностических сообщений.
* <p>
* Отвечает за публикацию диагностик с использованием {@code textDocument/publishDiagnostics}.
* Отвечает за публикацию диагностик с использованием {@code textDocument/publishDiagnostics},
* предоставление диагностик по запросу {@code textDocument/diagnostic}
* и уведомление об обновлении диагностик через {@code workspace/diagnostic/refresh}.
*
* @see <a href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics">PublishDiagnostics Notification specification</a>
* @see <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic">Diagnostic Pull Request specification</a>
* @see <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic_refresh">Diagnostic Refresh Request specification</a>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public final class DiagnosticProvider {

public static final String SOURCE = "bsl-language-server";

private final LanguageClientHolder clientHolder;
private final ClientCapabilitiesHolder clientCapabilitiesHolder;

private boolean clientSupportsRefresh;

/**
* Вычислить и опубликовать диагностики для документа.
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot давай попробуем сделать поддержку unchanged report. Где-то нужно хранить reportID и список диагностик с прошлого запуска. Предложи архитектуру решения, но НЕ РЕАЛИЗОВЫВАЙ. только схемы и описание.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architecture proposal for unchanged report support

Components needed:

  1. DiagnosticResultCache (new class)

    • Store: Map<URI, CachedDiagnosticResult>
    • CachedDiagnosticResult: { resultId: String, diagnostics: List, version: Integer }
    • Generate resultId: UUID or hash of diagnostics list
    • Invalidate on document changes (listen to rebuild events)
  2. DiagnosticProvider changes:

    • Accept optional previousResultId from DocumentDiagnosticParams
    • Compare current diagnostics with cached ones
    • Return RelatedUnchangedDocumentDiagnosticReport if match, else RelatedFullDocumentDiagnosticReport
  3. Flow:

    • Client sends previousResultId in request
    • Server checks cache for matching resultId + diagnostics
    • If unchanged: return UnchangedReport with same resultId
    • If changed: compute, cache new result with new resultId, return FullReport

Challenges:

  • Cache invalidation timing (document changes, configuration changes)
  • Memory management for cache size
  • Thread safety for concurrent requests

return new DocumentDiagnosticReport(report);
}

/**
* Опубликовать пустой список диагностик для документа.
*
Expand All @@ -65,6 +95,39 @@ public void publishEmptyDiagnosticList(DocumentContext documentContext) {
publishDiagnostics(documentContext, Collections::emptyList);
}

/**
* Обработчик события {@link LanguageServerInitializeRequestReceivedEvent}.
* <p>
* Проверяет поддержку клиентом 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}.
* <p>
* Отправляет клиенту запрос на обновление диагностик при изменении конфигурации.
*
* @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<List<Diagnostic>> diagnostics) {
clientHolder.execIfConnected(languageClient ->
languageClient.publishDiagnostics(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Loading