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 2be3d0562e6..1f67d797325 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 @@ -37,6 +37,7 @@ import org.eclipse.lsp4j.CodeActionOptions; import org.eclipse.lsp4j.CodeLensOptions; import org.eclipse.lsp4j.ColorProviderOptions; +import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.DefinitionOptions; import org.eclipse.lsp4j.DocumentFormattingOptions; import org.eclipse.lsp4j.DocumentLinkOptions; @@ -123,6 +124,7 @@ public CompletableFuture initialize(InitializeParams params) { capabilities.setRenameProvider(getRenameProvider(params)); capabilities.setInlayHintProvider(getInlayHintProvider()); capabilities.setExecuteCommandProvider(getExecuteCommandProvider()); + capabilities.setCompletionProvider(getCompletionProvider()); var result = new InitializeResult(capabilities, serverInfo); @@ -336,4 +338,13 @@ private ExecuteCommandOptions getExecuteCommandProvider() { executeCommandOptions.setWorkDoneProgress(Boolean.FALSE); return executeCommandOptions; } + + private CompletionOptions getCompletionProvider() { + var completionOptions = new CompletionOptions(); + completionOptions.setTriggerCharacters(List.of(".")); + completionOptions.setResolveProvider(Boolean.FALSE); + completionOptions.setWorkDoneProgress(Boolean.FALSE); + + return completionOptions; + } } 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 b2e534bd14b..0ef82af2794 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 @@ -32,6 +32,7 @@ import com.github._1c_syntax.bsl.languageserver.providers.CodeActionProvider; import com.github._1c_syntax.bsl.languageserver.providers.CodeLensProvider; import com.github._1c_syntax.bsl.languageserver.providers.ColorProvider; +import com.github._1c_syntax.bsl.languageserver.providers.CompletionProvider; import com.github._1c_syntax.bsl.languageserver.providers.DefinitionProvider; import com.github._1c_syntax.bsl.languageserver.providers.DiagnosticProvider; import com.github._1c_syntax.bsl.languageserver.providers.DocumentLinkProvider; @@ -60,6 +61,9 @@ import org.eclipse.lsp4j.ColorPresentation; import org.eclipse.lsp4j.ColorPresentationParams; import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; @@ -125,6 +129,7 @@ public class BSLTextDocumentService implements TextDocumentService, ProtocolExte private final ColorProvider colorProvider; private final RenameProvider renameProvider; private final InlayHintProvider inlayHintProvider; + private final CompletionProvider completionProvider; private final ExecutorService executorService = Executors.newCachedThreadPool(new CustomizableThreadFactory("text-document-service-")); @@ -370,6 +375,19 @@ public CompletableFuture> inlayHint(InlayHintParams params) { ); } + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams params) { + var documentContext = context.getDocument(params.getTextDocument().getUri()); + if (documentContext == null) { + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.supplyAsync( + () -> completionProvider.getCompletions(documentContext, params), + executorService + ); + } + @Override public void didOpen(DidOpenTextDocumentParams params) { var textDocumentItem = params.getTextDocument(); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MarkupContentBuilder.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MarkupContentBuilder.java index 8ef2864767f..d63624dfa84 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MarkupContentBuilder.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MarkupContentBuilder.java @@ -22,6 +22,7 @@ package com.github._1c_syntax.bsl.languageserver.hover; import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import com.github._1c_syntax.bsl.languageserver.references.model.Reference; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.SymbolKind; @@ -34,10 +35,11 @@ public interface MarkupContentBuilder { /** * Возвращает контент для всплывающего окна на основе символа. * - * @param symbol Символ, для которого нужно построить контент. + * @param reference Ссылка на символ, для которого нужно построить контент. + * @param symbol Символ с приведенным типом, для которого нужно построить контент. * @return Сконструированный контент. */ - MarkupContent getContent(T symbol); + MarkupContent getContent(Reference reference, T symbol); /** * Тип символа, на основе которого работает данный построитель. diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilder.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilder.java index de16d79d37f..f41e4c6eaec 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilder.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilder.java @@ -27,6 +27,7 @@ import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription; import com.github._1c_syntax.bsl.languageserver.context.symbol.description.ParameterDescription; import com.github._1c_syntax.bsl.languageserver.context.symbol.description.TypeDescription; +import com.github._1c_syntax.bsl.languageserver.references.model.Reference; import com.github._1c_syntax.bsl.languageserver.utils.MdoRefBuilder; import com.github._1c_syntax.bsl.languageserver.utils.Resources; import lombok.RequiredArgsConstructor; @@ -63,7 +64,7 @@ public class MethodSymbolMarkupContentBuilder implements MarkupContentBuilder addSectionIfNotEmpty(markupBuilder, trailingDescription)); + var types = typeResolver.findTypes(reference); + var typeDescription = getTypeDescription(types); + addSectionIfNotEmpty(markupBuilder, typeDescription); + String content = markupBuilder.toString(); return new MarkupContent(MarkupKind.MARKDOWN, content); @@ -112,6 +122,18 @@ private static String getLocation(VariableSymbol symbol) { ); } + private static String getTypeDescription(List types) { + var typeDescription = types.stream() + .map(Type::getName) + .collect(Collectors.joining(" | ")); + + if (!typeDescription.isEmpty()) { + typeDescription = "`" + typeDescription + "`"; + } + + return typeDescription; + } + private static void addSectionIfNotEmpty(StringJoiner markupBuilder, String newContent) { if (!newContent.isEmpty()) { markupBuilder.add(newContent); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProvider.java new file mode 100644 index 00000000000..998a07f5250 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProvider.java @@ -0,0 +1,86 @@ +package com.github._1c_syntax.bsl.languageserver.providers; + +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.SourceDefinedSymbol; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import com.github._1c_syntax.bsl.languageserver.types.KnownTypes; +import com.github._1c_syntax.bsl.languageserver.types.TypeResolver; +import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.parser.BSLLexer; +import lombok.RequiredArgsConstructor; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class CompletionProvider { + + private final TypeResolver typeResolver; + private final KnownTypes knownTypes; + + public Either, CompletionList> getCompletions(DocumentContext documentContext, CompletionParams params) { + + var completionList = new CompletionList(); + + var position = params.getPosition(); + var terminalNode = Trees.findTerminalNodeContainsPosition(documentContext.getAst(), position).orElseThrow(); + + if (terminalNode.getSymbol().getType() != BSLLexer.DOT) { + return Either.forRight(completionList); + } + + var previousToken = Trees.getPreviousTokenFromDefaultChannel(documentContext.getTokens(), terminalNode.getSymbol().getTokenIndex() - 1); + var completionItems = previousToken + .map(Ranges::create) + .map(Range::getStart) + .map(previousTokenPosition -> typeResolver.findTypes(documentContext.getUri(), previousTokenPosition)) + .stream() + .flatMap(Collection::stream) + .map(knownTypes::getSymbolByType) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(symbol -> getChildren(symbol).stream()) + .map(symbol -> { + var completionItem = new CompletionItem(); + completionItem.setLabel(symbol.getName()); + completionItem.setKind(getCompletionItemKind(symbol.getSymbolKind())); + + return completionItem; + }) + .toList(); + + completionList.setItems(completionItems); + + return Either.forRight(completionList); + } + + private CompletionItemKind getCompletionItemKind(SymbolKind symbolKind) { + return switch (symbolKind) { + case Class -> CompletionItemKind.Class; + case Method -> CompletionItemKind.Method; + case Variable -> CompletionItemKind.Variable; + case Module -> CompletionItemKind.Module; + default -> CompletionItemKind.Text; + }; + } + + private List getChildren(Symbol symbol) { + if (!(symbol instanceof SourceDefinedSymbol sourceDefinedSymbol)) { + return Collections.emptyList(); + } + + return sourceDefinedSymbol.getChildren(); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/HoverProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/HoverProvider.java index 2d180202559..cc11646e0d9 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/HoverProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/HoverProvider.java @@ -52,7 +52,7 @@ public Optional getHover(DocumentContext documentContext, HoverParams par var range = reference.getSelectionRange(); return Optional.ofNullable(markupContentBuilders.get(symbol.getSymbolKind())) - .map(markupContentBuilder -> markupContentBuilder.getContent(symbol)) + .map(markupContentBuilder -> markupContentBuilder.getContent(reference, symbol)) .map(content -> new Hover(content, range)); }); } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/KnownTypes.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/KnownTypes.java new file mode 100644 index 00000000000..dd2a83ef352 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/KnownTypes.java @@ -0,0 +1,39 @@ +package com.github._1c_syntax.bsl.languageserver.types; + +import com.github._1c_syntax.bsl.languageserver.context.events.DocumentContextContentChangedEvent; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import org.apache.commons.io.FilenameUtils; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class KnownTypes { + + private final Map knownTypes = new ConcurrentHashMap<>(); + + public void addType(Type type, Symbol symbol) { + knownTypes.put(type, symbol); + } + + public Optional getSymbolByType(Type type) { + return Optional.ofNullable(knownTypes.get(type)); + } + + public void clear() { + knownTypes.clear(); + } + + @EventListener + public void handleEvent(DocumentContextContentChangedEvent event) { + var documentContext = event.getSource(); + // TODO: this logic should be moved to somewhere else. It will break for BSL files. + var typeName = FilenameUtils.getBaseName(documentContext.getUri().getPath()); + var module = documentContext.getSymbolTree().getModule(); + + addType(new Type(typeName), module); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/Type.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/Type.java new file mode 100644 index 00000000000..2050fad7010 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/Type.java @@ -0,0 +1,29 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2024 + * 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.types; + +import lombok.Value; + +@Value +public class Type { + private final String name; +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolver.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolver.java new file mode 100644 index 00000000000..a0bfe99bc95 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolver.java @@ -0,0 +1,255 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2024 + * 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.types; + +import com.github._1c_syntax.bsl.languageserver.context.ServerContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Describable; +import com.github._1c_syntax.bsl.languageserver.context.symbol.SourceDefinedSymbol; +import com.github._1c_syntax.bsl.languageserver.context.symbol.Symbol; +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription; +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.TypeDescription; +import com.github._1c_syntax.bsl.languageserver.context.symbol.variable.VariableDescription; +import com.github._1c_syntax.bsl.languageserver.references.ReferenceIndex; +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.utils.Ranges; +import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.languageserver.utils.bsl.Constructors; +import com.github._1c_syntax.bsl.parser.BSLParser; +import lombok.RequiredArgsConstructor; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.eclipse.lsp4j.Position; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +@Component +@RequiredArgsConstructor +public class TypeResolver { + + private final ServerContext serverContext; + private final ReferenceResolver referenceResolver; + private final ReferenceIndex referenceIndex; + + // TODO: Create LRU cache for calculated types. + + public List findTypes(Reference reference) { + return calculateTypes(reference.getUri(), reference.getSymbol()); + } + + public List findTypes(URI uri, Position position) { + return referenceResolver.findReference(uri, position) + .stream() + .flatMap(reference -> calculateTypes(reference).stream()) + .distinct() + .toList(); + } + + private List calculateTypes(URI uri, Symbol symbol) { + if (symbol instanceof SourceDefinedSymbol sourceDefinedSymbol) { + return calculateTypes(uri, sourceDefinedSymbol); + } + return Collections.emptyList(); + } + + private List calculateTypes(URI uri, SourceDefinedSymbol symbol) { + + // variable description resolver + if (symbol instanceof Describable describableSymbol) { + var maybeDescription = describableSymbol.getDescription(); + if (maybeDescription.isPresent()) { + var description = maybeDescription.get(); + if (description instanceof VariableDescription variableDescription) { + // TODO: use new type information from new bsp-parser + var purposeDescription = variableDescription.getPurposeDescription(); + var typeName = Pattern.compile("^(\\S+)").matcher(purposeDescription).results() + .findFirst() + .map(MatchResult::group) + .orElse(""); + + if (!typeName.isEmpty()) { + return List.of(new Type(typeName)); + } + } + } + } + + // reference-based type resolver + if (symbol.getOwner().getContent() == null) { + return Collections.emptyList(); + } + var ast = symbol.getOwner().getAst(); + if (ast == null) { + return Collections.emptyList(); + } + + var position = symbol.getSelectionRange().getStart(); + + var typesOfCurrentReference = calculateTypes(uri, ast, position); + + var typesOfOtherReferences = referenceIndex.getReferencesTo(symbol).stream() + .filter(referenceTo -> referenceTo.getOccurrenceType() == OccurrenceType.DEFINITION) + .map(referenceTo -> calculateTypes(uri, ast, referenceTo.getSelectionRange().getStart())) + .flatMap(Collection::stream) + .toList(); + + return Stream.concat(typesOfCurrentReference.stream(), typesOfOtherReferences.stream()) + .distinct() + .toList(); + } + + private List calculateTypes(Reference reference) { + + var uri = reference.getUri(); + + // source defined symbol resolver + if (reference.isSourceDefinedSymbolReference()) { + return calculateTypes(uri, reference.getSourceDefinedSymbol().orElseThrow()); + } + + + // expression tree resolver + if (reference.getOccurrenceType() == OccurrenceType.DEFINITION) { + var document = serverContext.getDocument(uri); + var ast = document.getAst(); + if (ast == null) { + return Collections.emptyList(); + } + var position = reference.getSelectionRange().getStart(); + return calculateTypes(uri, ast, position); + } + + // no-op + return Collections.emptyList(); + } + + private List calculateTypes(URI uri, BSLParser.FileContext ast, Position position) { + return Trees.findTerminalNodeContainsPosition(ast, position) + .map(TerminalNode::getParent) + .map(ruleNode -> Trees.getRootParent(ruleNode, BSLParser.RULE_assignment)) + .map(BSLParser.AssignmentContext.class::cast) + .map(BSLParser.AssignmentContext::expression) + .map(expression -> calculateTypes(uri, expression)) + .orElseGet(Collections::emptyList); + } + + private List calculateTypes(URI uri, BSLParser.ExpressionContext expression) { + + // only simple cases for now. Use ExpressionTree in the future. + if (!expression.operation().isEmpty()) { + return Collections.emptyList(); + } + + // new-resolver + var typeName = newTypeName(expression); + if (!typeName.isEmpty()) { + Type type = new Type(typeName); + return List.of(type); + } + + // globalMethodCall resolver + var typeNames = returnedValue(uri, expression); + if (!typeNames.isEmpty()) { + return typeNames; + } + + // const-value resolver + var constValueContext = expression.member(0).constValue(); + if (constValueContext == null) { + return Collections.emptyList(); + } + + Type type = null; + if (constValueContext.DATETIME() != null) { + type = new Type("Дата"); + } else if (constValueContext.FALSE() != null || constValueContext.TRUE() != null) { + type = new Type("Булево"); + } else if (constValueContext.NULL() != null) { + type = new Type("null"); + } else if (constValueContext.numeric() != null) { + type = new Type("Число"); + } else if (constValueContext.string() != null) { + type = new Type("Строка"); + } else if (constValueContext.UNDEFINED() != null) { + type = new Type("Неопределено"); + } + + if (type != null) { + return List.of(type); + } + + return Collections.emptyList(); + + } + + private String newTypeName(BSLParser.ExpressionContext expression) { + var typeName = ""; + var newCtx = Trees.getNextNode(expression, expression, BSLParser.RULE_newExpression); + if (newCtx instanceof BSLParser.NewExpressionContext newExpression) { + typeName = Constructors.typeName(newExpression).orElse(""); + } + return typeName; + } + + private List returnedValue(URI uri, BSLParser.ExpressionContext expression) { + var complexIdentifier = expression.member(0).complexIdentifier(); + + if (complexIdentifier == null) { + return Collections.emptyList(); + } + + if (!complexIdentifier.modifier().isEmpty()) { + return Collections.emptyList(); + } + + var globalMethodCall = complexIdentifier.globalMethodCall(); + + if (globalMethodCall == null) { + return Collections.emptyList(); + } + + var calledMethod = referenceResolver.findReference(uri, Ranges.create(globalMethodCall.methodName()).getStart()); + + return calledMethod.filter(Reference::isSourceDefinedSymbolReference) + .flatMap(Reference::getSourceDefinedSymbol) + .filter(Describable.class::isInstance) + .map(Describable.class::cast) + .flatMap(Describable::getDescription) + .filter(MethodDescription.class::isInstance) + .map(MethodDescription.class::cast) + .map(MethodDescription::getReturnedValue) + .stream() + .flatMap(List::stream) + .map(TypeDescription::getName) + .map(Type::new) + .toList(); + + } + +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/Trees.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/Trees.java index e57dfa3a772..1230185798b 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/Trees.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/Trees.java @@ -28,6 +28,7 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; import org.antlr.v4.runtime.tree.TerminalNode; import org.antlr.v4.runtime.tree.Tree; import org.eclipse.lsp4j.Position; @@ -286,14 +287,14 @@ public static BSLParserRuleContext getRootParent(BSLParserRuleContext tnc) { * @return tnc - если родитель не найден, вернет null */ @Nullable - public static BSLParserRuleContext getRootParent(BSLParserRuleContext tnc, int ruleindex) { + public static BSLParserRuleContext getRootParent(RuleNode tnc, int ruleindex) { final var parent = tnc.getParent(); if (parent == null) { return null; } if (getRuleIndex(parent) == ruleindex) { - return parent; + return (BSLParserRuleContext) parent; } else { return getRootParent(parent, ruleindex); } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java index da295eb3092..c1902954090 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java @@ -21,11 +21,16 @@ */ package com.github._1c_syntax.bsl.languageserver.hover; +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.context.symbol.MethodSymbol; +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.util.CleanupContextBeforeClassAndAfterClass; import com.github._1c_syntax.bsl.languageserver.util.TestUtils; import com.github._1c_syntax.bsl.types.ModuleType; import jakarta.annotation.PostConstruct; +import org.eclipse.lsp4j.Location; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -59,9 +64,10 @@ void testContentFromDirectFile() { // given var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE); var methodSymbol = documentContext.getSymbolTree().getMethodSymbol("ИмяФункции").orElseThrow(); + var reference = getReference(documentContext, methodSymbol); // when - var content = markupContentBuilder.getContent(methodSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, methodSymbol).getValue(); assertThat(content).isNotEmpty(); @@ -122,9 +128,10 @@ void testContentFromManagerModule() { // given var documentContext = serverContext.getDocument("Catalog.Справочник1", ModuleType.ManagerModule).orElseThrow(); var methodSymbol = documentContext.getSymbolTree().getMethodSymbol("ТестЭкспортная").orElseThrow(); + var reference = getReference(documentContext, methodSymbol); // when - var content = markupContentBuilder.getContent(methodSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, methodSymbol).getValue(); // then assertThat(content).isNotEmpty(); @@ -146,9 +153,10 @@ void testMethodsFromCommonModule() { // given var documentContext = serverContext.getDocument("CommonModule.ПервыйОбщийМодуль", ModuleType.CommonModule).orElseThrow(); var methodSymbol = documentContext.getSymbolTree().getMethodSymbol("УстаревшаяПроцедура").orElseThrow(); + var reference = getReference(documentContext, methodSymbol); // when - var content = markupContentBuilder.getContent(methodSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, methodSymbol).getValue(); // then assertThat(content).isNotEmpty(); @@ -166,4 +174,12 @@ void testMethodsFromCommonModule() { assertThat(blocks.get(2)).isEqualTo("Процедура - Устаревшая процедура\n\n"); } + private static Reference getReference(DocumentContext documentContext, MethodSymbol methodSymbol) { + return Reference.of( + documentContext.getSymbolTree().getModule(), + methodSymbol, + new Location(documentContext.getUri().toString(), methodSymbol.getSelectionRange()), + OccurrenceType.DEFINITION + ); + } } \ No newline at end of file diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/VariableSymbolMarkupContentBuilderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/VariableSymbolMarkupContentBuilderTest.java index b8ade4cb595..4f501ddb472 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/VariableSymbolMarkupContentBuilderTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/VariableSymbolMarkupContentBuilderTest.java @@ -21,11 +21,16 @@ */ package com.github._1c_syntax.bsl.languageserver.hover; +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.context.symbol.VariableSymbol; +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.util.CleanupContextBeforeClassAndAfterClass; import com.github._1c_syntax.bsl.languageserver.util.TestUtils; import com.github._1c_syntax.bsl.types.ModuleType; import jakarta.annotation.PostConstruct; +import org.eclipse.lsp4j.Location; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -60,9 +65,10 @@ void testFileVarContentFromDirectFile_NoComments() { var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE); final var symbolTree = documentContext.getSymbolTree(); var varSymbol = symbolTree.getVariableSymbol("ИмяБезОписания", symbolTree.getModule()).orElseThrow(); + var reference = getReference(documentContext, varSymbol); // when - var content = markupContentBuilder.getContent(varSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, varSymbol).getValue(); assertThat(content).isNotEmpty(); @@ -87,9 +93,10 @@ void testFileVarContentFromDirectFile_OneCommentsStringFromRight() { var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE); final var symbolTree = documentContext.getSymbolTree(); var varSymbol = symbolTree.getVariableSymbol("Имя_ОписаниеСправаОднойСтрокой", symbolTree.getModule()).orElseThrow(); + var reference = getReference(documentContext, varSymbol); // when - var content = markupContentBuilder.getContent(varSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, varSymbol).getValue(); assertThat(content).isNotEmpty(); @@ -119,9 +126,10 @@ void testMethodVarContentFromDirectFile_2_comments_strings() { final var symbolTree = documentContext.getSymbolTree(); var methodSymbol = symbolTree.getMethodSymbol("ИмяФункции").orElseThrow(); var varSymbol = symbolTree.getVariableSymbol("Имя_ОписаниеСверхуДвеСтроки_Функция", methodSymbol).orElseThrow(); + var reference = getReference(documentContext, varSymbol); // when - var content = markupContentBuilder.getContent(varSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, varSymbol).getValue(); assertThat(content).isNotEmpty(); @@ -153,9 +161,10 @@ void testMethodVarContentFromDirectFile_3_comments_strings() { final var symbolTree = documentContext.getSymbolTree(); var methodSymbol = symbolTree.getMethodSymbol("ИмяФункции").orElseThrow(); var varSymbol = symbolTree.getVariableSymbol("Имя_ОписаниеСверхуТриСтрокиПоследняяПустая_Функция", methodSymbol).orElseThrow(); + var reference = getReference(documentContext, varSymbol); // when - var content = markupContentBuilder.getContent(varSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, varSymbol).getValue(); assertThat(content).isNotEmpty(); @@ -186,9 +195,10 @@ void testContentFromObjectModule() { var documentContext = serverContext.getDocument("Catalog.Справочник1", ModuleType.ObjectModule).orElseThrow(); final var symbolTree = documentContext.getSymbolTree(); var varSymbol = symbolTree.getVariableSymbol("ВалютаУчета", symbolTree.getModule()).orElseThrow(); + var reference = getReference(documentContext, varSymbol); // when - var content = markupContentBuilder.getContent(varSymbol).getValue(); + var content = markupContentBuilder.getContent(reference, varSymbol).getValue(); // then assertThat(content).isNotEmpty(); @@ -205,4 +215,12 @@ void testContentFromObjectModule() { assertThat(blocks.get(1)).matches("\\[Catalog.Справочник1]\\(.*Catalogs/.*/Ext/ObjectModule.bsl#\\d+\\)\n\n"); } + private static Reference getReference(DocumentContext documentContext, VariableSymbol variableSymbol) { + return Reference.of( + documentContext.getSymbolTree().getModule(), + variableSymbol, + new Location(documentContext.getUri().toString(), variableSymbol.getSelectionRange()), + OccurrenceType.DEFINITION + ); + } } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProviderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProviderTest.java new file mode 100644 index 00000000000..fa6e042e65b --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/CompletionProviderTest.java @@ -0,0 +1,57 @@ +package com.github._1c_syntax.bsl.languageserver.providers; + +import com.github._1c_syntax.bsl.languageserver.context.ServerContext; +import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass; +import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +import jakarta.annotation.PostConstruct; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.Position; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.nio.file.Paths; + +import static com.github._1c_syntax.bsl.languageserver.util.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest +@CleanupContextBeforeClassAndAfterClass +class CompletionProviderTest { + + @Autowired + private CompletionProvider completionProvider; + + @Autowired + private ServerContext serverContext; + + private static final String PATH_TO_FILE = "./src/test/resources/providers/completion.os"; + + @PostConstruct + void prepareServerContext() { + serverContext.setConfigurationRoot(Paths.get("src/test/resources/metadata/oslib")); + serverContext.populateContext(); + } + + @Test + void completionAfterDotOnOSClass() { + // given + var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE); + + var params = new CompletionParams(); + params.setPosition(new Position(3, 13)); + + // when + var completions = completionProvider.getCompletions(documentContext, params); + + // then + assertTrue(completions.isRight()); + + var completionList = completions.getRight(); + assertThat(completionList.getItems()).hasSize(1); + assertThat(completionList.getItems().get(0).getLabel()).isEqualTo("NewObject"); + assertThat(completionList.getItems().get(0).getKind()).isEqualTo(CompletionItemKind.Method); + } + +} \ No newline at end of file diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolverTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolverTest.java new file mode 100644 index 00000000000..bb4c8617e8c --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/types/TypeResolverTest.java @@ -0,0 +1,162 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2024 + * 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.types; + +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol; +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.util.TestUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class TypeResolverTest { + + @Autowired + private TypeResolver typeResolver; + + public static final String PATH_TO_FILE = "./src/test/resources/types/TypeResolver.os"; + + @Test + void simpleType() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + + // when + var types = typeResolver.findTypes(documentContext.getUri(), new Position(5, 10)); + + // then + assertThat(types).hasSize(1); + } + + @Test + void twoTypesOnReassignment() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + + // when + var types = typeResolver.findTypes(documentContext.getUri(), new Position(8, 4)); + + // then + assertThat(types).hasSize(2); + } + + @Test + void twoTypesOnPlaceOfUsage() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + + // when + var types = typeResolver.findTypes(documentContext.getUri(), new Position(10, 10)); + + // then + assertThat(types).hasSize(2); + } + + @Test + void twoTypesFromSymbol() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + var variableSymbol = documentContext.getSymbolTree().getVariableSymbol("ДваТипа", documentContext.getSymbolTree().getModule()).orElseThrow(); + var reference = getReference(documentContext, variableSymbol); + + // when + var types = typeResolver.findTypes(reference); + + // then + assertThat(types).hasSize(2); + } + + @Test + void twoAssignments() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + var variableSymbol = documentContext.getSymbolTree().getVariableSymbol("Переприсваивание", documentContext.getSymbolTree().getModule()).orElseThrow(); + var reference = getReference(documentContext, variableSymbol); + + // when + var types = typeResolver.findTypes(reference); + + // then + assertThat(types).hasSize(1); + } + + @Test + void newArray() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + var variableSymbol = documentContext.getSymbolTree().getVariableSymbol("ДругоеИмяМассива", documentContext.getSymbolTree().getModule()).orElseThrow(); + var reference = getReference(documentContext, variableSymbol); + + // when + var types = typeResolver.findTypes(reference); + + // then + assertThat(types).hasSize(1); + assertThat(types.get(0).getName()).isEqualTo("Массив"); + } + + @Test + void globalMethodCall() { + // given + var documentContext = TestUtils.getDocumentContextFromFile("./src/test/resources/types/TypeResolver.os"); + var variableSymbol = documentContext.getSymbolTree().getVariableSymbol("РезультатФункции", documentContext.getSymbolTree().getModule()).orElseThrow(); + var reference = getReference(documentContext, variableSymbol); + + // when + var types = typeResolver.findTypes(reference); + + // then + assertThat(types).hasSize(1); + assertThat(types.get(0).getName()).isEqualTo("Строка"); + } + + @Test + void varWithDescription() { + // given + var documentContext = TestUtils.getDocumentContextFromFile(PATH_TO_FILE); + var variableSymbol = documentContext.getSymbolTree().getVariableSymbol("ПеременнаяСОписанием", documentContext.getSymbolTree().getModule()).orElseThrow(); + var reference = getReference(documentContext, variableSymbol); + + // when + var types = typeResolver.findTypes(reference); + + // then + assertThat(types).hasSize(1); + assertThat(types.get(0).getName()).isEqualTo("Строка"); + } + + private static Reference getReference(DocumentContext documentContext, VariableSymbol variableSymbol) { + return Reference.of( + documentContext.getSymbolTree().getModule(), + variableSymbol, + new Location(documentContext.getUri().toString(), variableSymbol.getSelectionRange()), + OccurrenceType.DEFINITION + ); + } +} \ No newline at end of file diff --git "a/src/test/resources/metadata/oslib/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\276\320\271\320\232\320\273\320\260\321\201\321\201.os" "b/src/test/resources/metadata/oslib/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\276\320\271\320\232\320\273\320\260\321\201\321\201.os" new file mode 100644 index 00000000000..86aa12c6c29 --- /dev/null +++ "b/src/test/resources/metadata/oslib/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\276\320\271\320\232\320\273\320\260\321\201\321\201.os" @@ -0,0 +1,17 @@ +Перем ЭкспортнаяПеременная Экспорт; +Перем НеЭкспортнаяПеременная; + +Функция ЭкспортнаяФункция() Экспорт + Возврат ""; +КонецФункции + +Функция НеЭкспортнаяФункция() + Возврат ""; +КонецФункции + +Процедура ЭкспортнаяПроцедура() Экспорт +КонецПроцедуры + +Процедура НеЭкспортнаяПроцедура() +КонецПроцедуры + diff --git a/src/test/resources/providers/completion.os b/src/test/resources/providers/completion.os new file mode 100644 index 00000000000..b5109c7caa1 --- /dev/null +++ b/src/test/resources/providers/completion.os @@ -0,0 +1,4 @@ +#Использовать "metadata/oslib" + +КакойТоКласс = Новый МойКласс(); +КакойТоКласс. diff --git a/src/test/resources/types/TypeResolver.os b/src/test/resources/types/TypeResolver.os new file mode 100644 index 00000000000..f3f37b4ee24 --- /dev/null +++ b/src/test/resources/types/TypeResolver.os @@ -0,0 +1,34 @@ +// Строка - какая-то переменная +Перем ПеременнаяСОписанием; + +ИмяКорневойАннотации = "Завязь"; + +Сообщить(ИмяКорневойАннотации); + +ДваТипа = "Строка"; +ДваТипа = 0; + +Сообщить(ДваТипа); + +Переприсваивание = 0; +Переприсваивание = 0; + +Сообщить(Переприсваивание); + +ДругоеИмяМассива = Новый Массив; +ДругоеИмяМассива.Добавить(1); + +РезультатФункции = КакаяТоФункция(); + +Сообщить(РезультатФункции); + +Сообщить(ПеременнаяСОписанием); + +// Важная функция +// +// Возвращаемое значение: +// Строка - что-то там. +// +Функция КакаяТоФункция() + Возврат "Какая-то функция"; +КонецФункции \ No newline at end of file