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/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/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/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/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" + +КакойТоКласс = Новый МойКласс(); +КакойТоКласс.