diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplier.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplier.java new file mode 100644 index 00000000000..892e30a57a9 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplier.java @@ -0,0 +1,156 @@ +package com.github._1c_syntax.bsl.languageserver.codeactions; + +import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.context.symbol.annotations.Annotation; +import com.github._1c_syntax.bsl.languageserver.context.symbol.variable.VariableKind; +import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; +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.BSLParser; +import lombok.RequiredArgsConstructor; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; + +@Component +@RequiredArgsConstructor +public class GenerateFunctionSupplier implements CodeActionSupplier { + + final private ReferenceResolver referenceResolver; + + @Override + public List getCodeActions(CodeActionParams params, DocumentContext documentContext) { + + var start = params.getRange().getStart(); + if (start == null) { + return Collections.emptyList(); + } + + var parseTree = documentContext.getAst(); + var node = Trees.findTerminalNodeContainsPosition(parseTree, start); + + if(nodeIsMethod(node) && (referenceResolver.findReference(documentContext.getUri(), start).isEmpty())){ + return codeActions(documentContext, parseTree, node); + } + + return Collections.emptyList(); + } + + private List codeActions(DocumentContext documentContext, BSLParser.FileContext parseTree, Optional node){ + + var methodContext = ((BSLParser.GlobalMethodCallContext) node.get().getParent().getParent()); + var methodName = methodContext.methodName().getText(); + var methodParams = getMethodParams(methodContext); + + // TODO: Корректное позиционирование - после текущего метода, после переменных, после инклудов, в начале. + var position = getNewMethodPosition(documentContext, methodContext); + + // TODO: Двуязычность. + var funcCodeAction = codeAction(documentContext, position, "Generate function", getMethodContent(methodName, methodParams, true)); + var procCodeAction = codeAction(documentContext, position, "Generate procedure", getMethodContent(methodName, methodParams, false)); + + return List.of(funcCodeAction, procCodeAction); + } + + private CodeAction codeAction(DocumentContext documentContext, Range position, String title ,String methodContent){ + + TextEdit textEdit = new TextEdit(); + + textEdit.setRange(position); + textEdit.setNewText(methodContent); + + WorkspaceEdit edit = new WorkspaceEdit(); + + Map> changes = Map.of(documentContext.getUri().toString(), Collections.singletonList(textEdit)); + edit.setChanges(changes); + + var codeAction = new CodeAction(title); + codeAction.setKind(CodeActionKind.Refactor); + codeAction.setEdit(edit); + + return codeAction; + } + + private String getMethodParams(BSLParser.GlobalMethodCallContext methodContext){ + + var callParams = methodContext.doCall().callParamList().callParam(); + var joiner = new StringJoiner(", "); + + for (BSLParser.CallParamContext callParam: callParams) { + joiner.add(callParam.getText()); + } + + return joiner.toString(); + } + + private String getMethodContent(String methodName, String methodParams, boolean isFunction){ + + return String.format(isFunction ? functionTemplate() : procedureTemplate(), methodName, methodParams); + + } + + private String functionTemplate(){ + return "%nФункция %s(%s)%n%n //TODO: содержание метода%n%n Возврат Неопределено;%n%nКонецФункции%n"; + } + + private String procedureTemplate(){ + return "%nПроцедура %s(%s)%n%n //TODO: содержание метода%n%nКонецПроцедуры%n"; + } + private Range getNewMethodPosition(DocumentContext documentContext, BSLParser.GlobalMethodCallContext methodContext){ + + var ancestorRuleSub = Trees.getAncestorByRuleIndex(methodContext, BSLParser.RULE_sub); + + if (ancestorRuleSub != null){ + var line = ancestorRuleSub.getStop().getLine(); + return Ranges.create(line, 1, line, 1); + } + + var methods = documentContext.getSymbolTree().getMethods(); + + if (!methods.isEmpty()){ + var lastMethod = methods.get(methods.size() - 1); + var line = lastMethod.getRange().getEnd().getLine() + 1; + return Ranges.create(line, 0, line, 0); + } + + var variables = documentContext.getSymbolTree().getVariables() + .stream().filter(e -> e.getKind() == VariableKind.MODULE).toList(); + + if (!variables.isEmpty()){ + var lastVariable = variables.get(variables.size() - 1); + var line = lastVariable.getRange().getEnd().getLine() + 1; + return Ranges.create(line, 0, line, 0); + } + + var annotations = documentContext.getAst().moduleAnnotations(); + if (annotations != null) { + var uses = annotations.use(); + + if (!uses.isEmpty()) { + var lastUse = uses.get(uses.size() - 1); + var line = lastUse.stop.getLine(); + return Ranges.create(line, 0, line, 0); + } + } + + return Ranges.create(0, 0, 0, 0); + + } + + private boolean nodeIsMethod(Optional node){ + return node.map(TerminalNode::getParent) + .filter(BSLParser.MethodNameContext.class::isInstance) + .isPresent(); + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplierTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplierTest.java new file mode 100644 index 00000000000..6184387b1a0 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/codeactions/GenerateFunctionSupplierTest.java @@ -0,0 +1,264 @@ +package com.github._1c_syntax.bsl.languageserver.codeactions; + +import com.github._1c_syntax.bsl.languageserver.configuration.Language; +import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration; +import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextEdit; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class GenerateFunctionSupplierTest { + + @Autowired + private LanguageServerConfiguration configuration; + + @Autowired + private GenerateFunctionSupplier codeActionSupplier; + + @Test + void testGetCodeAction() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(14 , 5, 24)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat(codeActions) + .hasSize(2) + .anyMatch(codeAction -> codeAction.getTitle().equals("Generate function")) + .anyMatch(codeAction -> codeAction.getTitle().equals("Generate procedure")); + } + + @Test + void testCodeActionPositionFromBodyEmpty() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier_Empty.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(6 , 1, 19)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getRange().getStart().getLine() == 0) + ; + + } + + @Test + void testCodeActionPositionFromBodyUse() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier_Use.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(9 , 1, 19)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getRange().getStart().getLine() == 2) + ; + + } + + @Test + void testCodeActionPositionFromBodyVars() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier_UseAndVars.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(12 , 1, 19)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getRange().getStart().getLine() == 5) + ; + + } + + @Test + void testCodeActionPositionFromBodyMethod() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(24 , 1, 20)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getRange().getStart().getLine() == 21) + ; + + } + + @Test + void testCodeActionPositionFromMethod() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(16 , 5, 24)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getRange().getStart().getLine() == 21) + ; + + } + + @Test + void testCodeActionHasParams() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(16 , 5, 24)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat((((List) (codeActions.get(0).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getNewText().indexOf("Параметр, ВторойПараметр") > 0) + ; + assertThat((((List) (codeActions.get(1).getEdit().getChanges().values()).toArray()[0]))) + .allMatch(textedit -> ((TextEdit) textedit).getNewText().indexOf("Параметр, ВторойПараметр") > 0) + ; + } + + @Test + void testGetNoCodeActionOnExistsMethod() { + // given + configuration.setLanguage(Language.EN); + + String filePath = "./src/test/resources/suppliers/GenerateFunctionSupplier.bsl"; + var documentContext = TestUtils.getDocumentContextFromFile(filePath); + + List diagnostics = new ArrayList<>(); + + TextDocumentIdentifier textDocumentIdentifier = new TextDocumentIdentifier(documentContext.getUri().toString()); + + CodeActionContext codeActionContext = new CodeActionContext(); + codeActionContext.setDiagnostics(diagnostics); + + CodeActionParams params = new CodeActionParams(); + params.setRange(Ranges.create(12 , 5, 22)); + params.setTextDocument(textDocumentIdentifier); + params.setContext(codeActionContext); + + // when + List codeActions = codeActionSupplier.getCodeActions(params, documentContext); + + assertThat(codeActions) + .hasSize(0); + } + +} diff --git a/src/test/resources/suppliers/GenerateFunctionSupplier.bsl b/src/test/resources/suppliers/GenerateFunctionSupplier.bsl new file mode 100644 index 00000000000..778c314e2b0 --- /dev/null +++ b/src/test/resources/suppliers/GenerateFunctionSupplier.bsl @@ -0,0 +1,25 @@ + +Функция СуществующийМетод(Параметр) + Возврат Параметр + 1; +КонецФункции + +Процедура СуществующаяПроцедура() + +КонецПроцедуры + +Процедура Инициализация() + + Параметр = СуществующийМетод(1); + СуществующаяПроцедура(); + + НеСуществующийМетод1(Параметр); + НеСуществующийМетод1(Параметр1); + НеСуществующийМетод2(Параметр, ВторойПараметр); + + Результат = НеСуществующийМетод1("Строка"); + +КонецПроцедуры + +Инициализация(); + +НеСуществующийМетод3(); \ No newline at end of file diff --git a/src/test/resources/suppliers/GenerateFunctionSupplier_Empty.bsl b/src/test/resources/suppliers/GenerateFunctionSupplier_Empty.bsl new file mode 100644 index 00000000000..94d531977d7 --- /dev/null +++ b/src/test/resources/suppliers/GenerateFunctionSupplier_Empty.bsl @@ -0,0 +1,7 @@ +в = 1; + +Если а = 1 Тогда + г = 1; +КонецЕсли; + +НеСуществующийМетод(); \ No newline at end of file diff --git a/src/test/resources/suppliers/GenerateFunctionSupplier_Use.bsl b/src/test/resources/suppliers/GenerateFunctionSupplier_Use.bsl new file mode 100644 index 00000000000..6cff31fa5ce --- /dev/null +++ b/src/test/resources/suppliers/GenerateFunctionSupplier_Use.bsl @@ -0,0 +1,10 @@ +#Использовать "." +#Использовать autumn + +в = 1; + +Если а = 1 Тогда + г = 1; +КонецЕсли; + +НеСуществующийМетод(); \ No newline at end of file diff --git a/src/test/resources/suppliers/GenerateFunctionSupplier_UseAndVars.bsl b/src/test/resources/suppliers/GenerateFunctionSupplier_UseAndVars.bsl new file mode 100644 index 00000000000..a15fbf75302 --- /dev/null +++ b/src/test/resources/suppliers/GenerateFunctionSupplier_UseAndVars.bsl @@ -0,0 +1,13 @@ +#Использовать "." +#Использовать autumn + +Перем а; +Перем б; + +в = 1; + +Если а = 1 Тогда + г = 1; +КонецЕсли; + +НеСуществующийМетод(); \ No newline at end of file