diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java index c7bf5d8afd5..c3fccdb33e5 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProvider.java @@ -27,6 +27,7 @@ import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; import com.github._1c_syntax.bsl.languageserver.references.model.Reference; +import com.github._1c_syntax.bsl.languageserver.types.symbol.PlatformMemberSymbol; import lombok.RequiredArgsConstructor; import org.eclipse.lsp4j.ClientCapabilities; import org.eclipse.lsp4j.DefinitionCapabilities; @@ -110,12 +111,47 @@ private List findLocationLinks(DocumentContext documentContext, De Position position = params.getPosition(); return referenceResolver.findReference(documentContext.getUri(), position) + .map(DefinitionProvider::unwrapPlatformMemberSource) .filter(Reference::isSourceDefinedSymbolReference) .map(DefinitionProvider::toLocationLink) .map(Collections::singletonList) .orElse(Collections.emptyList()); } + /** + * Если ссылка указывает на член платформенного/конфигурационного типа + * (синтетический {@link PlatformMemberSymbol}, разрешённый через + * {@code TypeService.memberAt}) и у его дескриптора есть source-defined + * символ-источник (метод OneScript-класса, экспортная переменная-свойство + * и т.п. — см. {@code OScriptModuleMembersProvider}), возвращает ссылку, + * перенаправленную на этот источник; иначе возвращает ссылку без изменений. + *

+ * Так основной пайплайн перехода не меняется: платформенные члены без + * источника остаются {@link PlatformMemberSymbol} и отсекаются фильтром + * {@link Reference#isSourceDefinedSymbolReference()} (остаётся только hover). + *

+ * Обсуждение и дизайн: https://github.com/1c-syntax/bsl-language-server/pull/4197 + * + * @param reference исходная ссылка под курсором. + * @return ссылка на source-defined источник либо исходная ссылка. + */ + private static Reference unwrapPlatformMemberSource(Reference reference) { + if (!(reference.symbol() instanceof PlatformMemberSymbol platformMember)) { + return reference; + } + return platformMember.getDescriptor().getSourceSymbol() + .filter(SourceDefinedSymbol.class::isInstance) + .map(SourceDefinedSymbol.class::cast) + .map(sourceSymbol -> new Reference( + reference.from(), + sourceSymbol, + reference.uri(), + reference.selectionRange(), + reference.occurrenceType() + )) + .orElse(reference); + } + private static LocationLink toLocationLink(Reference reference) { SourceDefinedSymbol symbol = (SourceDefinedSymbol) reference.symbol(); diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java new file mode 100644 index 00000000000..008bfd8c182 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/DefinitionProviderUnitTest.java @@ -0,0 +1,146 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2026 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.providers; + +import com.github._1c_syntax.bsl.languageserver.ClientCapabilitiesHolder; +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; +import com.github._1c_syntax.bsl.languageserver.references.model.OccurrenceType; +import com.github._1c_syntax.bsl.languageserver.references.model.Reference; +import com.github._1c_syntax.bsl.languageserver.types.model.MemberDescriptor; +import com.github._1c_syntax.bsl.languageserver.types.symbol.PlatformMemberSymbol; +import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Mockito-юнит для {@link DefinitionProvider}: переход к определению члена + * платформенного/конфигурационного типа, разрешённого через + * {@code TypeService.memberAt} в синтетический {@link PlatformMemberSymbol}. + *

+ * Покрывает разворот ссылки на source-defined символ-источник дескриптора + * без поднятия полного {@code ServerContext}. + * Обсуждение: https://github.com/1c-syntax/bsl-language-server/pull/4197 + */ +class DefinitionProviderUnitTest { + + private static final URI SOURCE_URI = URI.create("file:///lib/asserts/assertion.os"); + + @Test + void definitionFollowsSourceSymbolOfPlatformMember() { + // given — член (метод OS-класса) с прикреплённым source-defined символом-источником + var sourceRange = Ranges.create(10, 0, 40); + var sourceSelectionRange = Ranges.create(10, 9, 17); + var sourceSymbol = sourceDefinedMethod(sourceRange, sourceSelectionRange); + + var memberRange = Ranges.create(3, 24, 32); + var reference = platformMemberReference(sourceSymbol, memberRange); + + var provider = providerReturning(reference, true); + + var params = new DefinitionParams(); + params.setPosition(new Position(3, 26)); + + // when + var definitions = provider.getDefinition(mock(DocumentContext.class), params); + + // then — переход ведёт к источнику, origin = диапазон обращения к члену + assertThat(definitions.isRight()).isTrue(); + assertThat(definitions.getRight()).hasSize(1); + + var link = definitions.getRight().get(0); + assertThat(link.getTargetUri()).isEqualTo(SOURCE_URI.toString()); + assertThat(link.getTargetRange()).isEqualTo(sourceRange); + assertThat(link.getTargetSelectionRange()).isEqualTo(sourceSelectionRange); + assertThat(link.getOriginSelectionRange()).isEqualTo(memberRange); + } + + @Test + void definitionEmptyForPlatformMemberWithoutSource() { + // given — платформенный член без символа-источника (нет объявления в исходниках) + var memberRange = Ranges.create(3, 24, 32); + var member = new PlatformMemberSymbol("Добавить", null, + MemberDescriptor.method("Добавить"), -1, List.of()); + var reference = new Reference( + mock(MethodSymbol.class), member, SOURCE_URI, memberRange, OccurrenceType.REFERENCE); + + var provider = providerReturning(reference, true); + + var params = new DefinitionParams(); + params.setPosition(new Position(3, 26)); + + // when + var definitions = provider.getDefinition(mock(DocumentContext.class), params); + + // then — перехода нет (остаётся только hover) + assertThat(definitions.isRight()).isTrue(); + assertThat(definitions.getRight()).isEmpty(); + } + + private static MethodSymbol sourceDefinedMethod(Range range, Range selectionRange) { + var owner = mock(DocumentContext.class); + when(owner.getUri()).thenReturn(SOURCE_URI); + + var sourceSymbol = mock(MethodSymbol.class); + when(sourceSymbol.getOwner()).thenReturn(owner); + when(sourceSymbol.getRange()).thenReturn(range); + when(sourceSymbol.getSelectionRange()).thenReturn(selectionRange); + return sourceSymbol; + } + + private static Reference platformMemberReference(Symbol sourceSymbol, Range memberRange) { + var descriptor = MemberDescriptor.method("ИмеетТип").withSourceSymbol(sourceSymbol); + var member = new PlatformMemberSymbol("ИмеетТип", null, descriptor, 1, List.of()); + return new Reference( + mock(MethodSymbol.class), member, SOURCE_URI, memberRange, OccurrenceType.REFERENCE); + } + + private static DefinitionProvider providerReturning(Reference reference, boolean linkSupport) { + var referenceResolver = mock(ReferenceResolver.class); + when(referenceResolver.findReference(any(), any())).thenReturn(Optional.of(reference)); + + var capabilitiesHolder = mock(ClientCapabilitiesHolder.class); + var provider = new DefinitionProvider(referenceResolver, capabilitiesHolder); + if (linkSupport) { + var capabilities = new org.eclipse.lsp4j.ClientCapabilities(); + var textDocument = new org.eclipse.lsp4j.TextDocumentClientCapabilities(); + textDocument.setDefinition(new org.eclipse.lsp4j.DefinitionCapabilities(false, true)); + capabilities.setTextDocument(textDocument); + when(capabilitiesHolder.getCapabilities()).thenReturn(Optional.of(capabilities)); + provider.handleInitializeEvent(); + } + return provider; + } +}