diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF index b4d4114831..90b241a842 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF @@ -23,7 +23,10 @@ Require-Bundle: org.eclipse.jdt.launching;bundle-version="3.8.0", org.eclipse.ui.editors, org.springsource.ide.eclipse.commons.core, org.eclipse.e4.ui.css.swt.theme, - org.eclipse.swt + org.eclipse.swt, + org.eclipse.ui.workbench, + org.eclipse.jdt.core.manipulation, + org.eclipse.ltk.core.refactoring Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Export-Package: org.springframework.tooling.ls.eclipse.commons, diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/InjectBean.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/InjectBean.java new file mode 100644 index 0000000000..1eac05da94 --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/InjectBean.java @@ -0,0 +1,253 @@ +package org.springframework.tooling.ls.eclipse.commons; + +import java.net.URI; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.Assignment; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FieldAccess; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jdt.core.refactoring.CompilationUnitChange; +import org.eclipse.jdt.internal.corext.dom.IASTSharedValues; +import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages; +import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; +import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser; +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.text.edits.DeleteEdit; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.TextEditGroup; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.PartInitException; +import org.springframework.tooling.jdt.ls.commons.Logger; +import org.springframework.tooling.jdt.ls.commons.resources.ResourceUtils; + +@SuppressWarnings({ "restriction", "unchecked" }) +public final class InjectBean { + + private final Logger logger; + + private String fieldTypeDeclarationName = null; + + public InjectBean(Logger logger) { + this.logger = logger; + } + + private void createFieldDeclaration(CompilationUnitRewrite cuRewrite, CompilationUnit domCu, + String typeName, String fieldType, String fieldName) throws JavaModelException { + AST ast= cuRewrite.getAST(); + VariableDeclarationFragment variableDeclarationFragment= ast.newVariableDeclarationFragment(); + variableDeclarationFragment.setName(ast.newSimpleName(fieldName)); + + FieldDeclaration fieldDeclaration= ast.newFieldDeclaration(variableDeclarationFragment); + + fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD)); + fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.FINAL_KEYWORD)); + + ImportRewrite importRewrite= cuRewrite.getImportRewrite(); + fieldTypeDeclarationName = importRewrite.addImport(fieldType); + fieldDeclaration.setType(ast.newSimpleType(ast.newName(fieldTypeDeclarationName))); + + AbstractTypeDeclaration parent = ((Stream) domCu.types().stream() + .filter(AbstractTypeDeclaration.class::isInstance) + .map(AbstractTypeDeclaration.class::cast)) + .filter(td -> typeName.equals(td.getName().getIdentifier())) + .findFirst() + .orElse(null); + Assert.isNotNull(parent); + ListRewrite listRewrite= cuRewrite.getASTRewrite().getListRewrite(parent, parent.getBodyDeclarationsProperty()); + TextEditGroup msg= cuRewrite.createGroupDescription(RefactoringCoreMessages.ExtractConstantRefactoring_declare_constant); + listRewrite.insertFirst(fieldDeclaration, msg); + } + + private void maybeAddConstructor(CompilationUnitRewrite cuRewrite, CompilationUnit domCu, + String typeName, String fieldType, String fieldName) { + TypeDeclaration typeDom = (TypeDeclaration) ((Stream) domCu.types().stream() + .filter(TypeDeclaration.class::isInstance) + .map(TypeDeclaration.class::cast)) + .filter(td -> typeName.equals(td.getName().getIdentifier())) + .findFirst() + .orElse(null); + + MethodDeclaration constructor = null; + boolean parameterAdded = false; + for (MethodDeclaration m : typeDom.getMethods()) { + if (m.isConstructor()) { + IMethodBinding methodBinding = m.resolveBinding(); + if (methodBinding != null) { + boolean autowired = Arrays.stream(methodBinding.getAnnotations()).anyMatch(a -> "org.springframework.beans.factory.annotation.Autowired".equals(a.getAnnotationType().getQualifiedName())); + boolean hasParameter = Arrays.stream(methodBinding.getParameterTypes()).anyMatch(t -> fieldType.equals(t.getQualifiedName())); + if (autowired) { + constructor = m; + parameterAdded = hasParameter; + break; + } else { + if (constructor == null && !parameterAdded) { + constructor = m; + parameterAdded = hasParameter; + } + } + } + } + } + + if (constructor == null) { + AST ast = domCu.getAST(); + + MethodDeclaration newConstructor = ast.newMethodDeclaration(); + newConstructor.setConstructor(true); + newConstructor.setName(ast.newSimpleName(typeName)); + newConstructor.parameters().add(createVariableDeclaration(ast, fieldName)); + newConstructor.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD)); + Block block = ast.newBlock(); + block.statements().add(ast.newExpressionStatement(createAssignment(ast, fieldName))); + newConstructor.setBody(block); + + ListRewrite listRewrite= cuRewrite.getASTRewrite().getListRewrite(typeDom, TypeDeclaration.BODY_DECLARATIONS_PROPERTY); + TextEditGroup msg= cuRewrite.createGroupDescription(RefactoringCoreMessages.ExtractConstantRefactoring_declare_constant); + if (typeDom.getMethods().length == 0) { + listRewrite.insertLast(newConstructor, msg); + } else { + listRewrite.insertBefore(newConstructor, typeDom.getMethods()[0], msg); + } + } else { + AST ast = constructor.getAST(); + SingleVariableDeclaration newParam = createVariableDeclaration(ast, fieldName); + + ListRewrite listRewrite= cuRewrite.getASTRewrite().getListRewrite(constructor, MethodDeclaration.PARAMETERS_PROPERTY); + TextEditGroup msg= cuRewrite.createGroupDescription(RefactoringCoreMessages.ExtractConstantRefactoring_declare_constant); + List parameters = constructor.parameters(); + listRewrite.insertAfter(newParam, parameters.get(parameters.size() - 1), msg); + + Block block = constructor.getBody(); + listRewrite = cuRewrite.getASTRewrite().getListRewrite(block, Block.STATEMENTS_PROPERTY); + listRewrite.insertLast(ast.newExpressionStatement(createAssignment(ast, fieldName)), msg); + } + } + + private SingleVariableDeclaration createVariableDeclaration(AST ast, String fieldName) { + SingleVariableDeclaration newParam = ast.newSingleVariableDeclaration(); + newParam.setName(ast.newSimpleName(fieldName)); + newParam.setType(ast.newSimpleType(ast.newSimpleName(fieldTypeDeclarationName))); + return newParam; + } + + private Assignment createAssignment(AST ast, String fieldName) { + Assignment assign = ast.newAssignment(); + assign.setRightHandSide(ast.newSimpleName(fieldName)); + FieldAccess thisField = ast.newFieldAccess(); + thisField.setName(ast.newSimpleName(fieldName)); + thisField.setExpression(ast.newThisExpression()); + assign.setLeftHandSide(thisField); + return assign; + } + + public TextDocumentEdit computeEdits(String docUri, String typeName, String fieldType, String fieldName) { + try { + URI resourceUri = URI.create(docUri); + IJavaProject project = ResourceUtils.getJavaProject(resourceUri); + + Optional optEditorInput = LSPEclipseUtils.findOpenEditorsFor(resourceUri).stream().map(e -> { + try { + return e.getEditorInput(); + } catch (PartInitException e1) { + return null; + } + }).findFirst(); + + if (project != null && optEditorInput.isPresent()) { + ICompilationUnit cu = JavaPlugin.getDefault().getWorkingCopyManager().getWorkingCopy(optEditorInput.get()); + RefactoringASTParser parser= new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL); + CompilationUnit domCu = parser.parse(cu, true); + + + CompilationUnitRewrite cuRewrite = new CompilationUnitRewrite(null, cu, domCu, Map.of()); + + createFieldDeclaration(cuRewrite, domCu, typeName, fieldType, fieldName); + + maybeAddConstructor(cuRewrite, domCu, typeName, fieldType, fieldName); + +// TextEdit edit = cuRewrite.getASTRewrite().rewriteAST(); + + CompilationUnitChange c = cuRewrite.createChange(false); + List textEdits = convertTextEdit(domCu, c.getEdit()); + + logger.log("Here"); + return textEdits.isEmpty() ? null : new TextDocumentEdit(new VersionedTextDocumentIdentifier(docUri, 0), textEdits); + } + + } catch (Exception e) { + logger.log(e); + } + return null; + } + + private List convertTextEdit(CompilationUnit domCu, TextEdit te) { + LinkedList edits = new LinkedList<>(); + org.eclipse.lsp4j.TextEdit edit = convertSingleEdit(domCu, te); + if (edit != null) { + edits.add(edit); + } + for (TextEdit c : te.getChildren()) { + edits.addAll(convertTextEdit(domCu, c)); + } + return edits; + } + + private org.eclipse.lsp4j.TextEdit convertSingleEdit(CompilationUnit domCu, TextEdit te) { + if (te instanceof DeleteEdit de) { + org.eclipse.lsp4j.TextEdit edit = new org.eclipse.lsp4j.TextEdit(); + int startLine = domCu.getLineNumber(de.getOffset()); + int startColumn = domCu.getColumnNumber(de.getOffset()) - 1; + int endLine = domCu.getLineNumber(de.getOffset() + de.getLength()); + int endColumn = domCu.getColumnNumber(de.getOffset() + de.getLength()) - 1; + edit.setNewText(""); + edit.setRange(new Range(new Position(startLine, startColumn), new Position(endLine, endColumn))); + return edit; + } else if (te instanceof ReplaceEdit re) { + org.eclipse.lsp4j.TextEdit edit = new org.eclipse.lsp4j.TextEdit(); + int startLine = domCu.getLineNumber(re.getOffset()); + int startColumn = domCu.getColumnNumber(re.getOffset()) - 1; + int endLine = domCu.getLineNumber(re.getOffset() + re.getLength()); + int endColumn = domCu.getColumnNumber(re.getOffset() + re.getLength()) - 1; + edit.setNewText(re.getText()); + edit.setRange(new Range(new Position(startLine, startColumn), new Position(endLine, endColumn))); + return edit; + } else if (te instanceof InsertEdit ie) { + org.eclipse.lsp4j.TextEdit edit = new org.eclipse.lsp4j.TextEdit(); + int startLine = domCu.getLineNumber(ie.getOffset()); + int startColumn = domCu.getColumnNumber(ie.getOffset()) - 1; + edit.setNewText(ie.getText()); + edit.setRange(new Range(new Position(startLine, startColumn), new Position(startLine, startColumn))); + return edit; + } + return null; + } + +} diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java index 5cd242bec1..5e948402ff 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java @@ -59,6 +59,7 @@ import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MarkupKind; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Display; @@ -77,6 +78,7 @@ import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; import org.springframework.ide.vscode.commons.protocol.java.ClasspathListenerParams; import org.springframework.ide.vscode.commons.protocol.java.Gav; +import org.springframework.ide.vscode.commons.protocol.java.InjectBeanParams; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteData; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteParams; import org.springframework.ide.vscode.commons.protocol.java.JavaDataParams; @@ -646,4 +648,11 @@ public CompletableFuture> projectGAV(ProjectGavParams params) { return BuildInfo.projectGAV(params, executor, Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance)); } + @Override + public CompletableFuture injectBean(InjectBeanParams params) { + return CompletableFuture + .supplyAsync(() -> new InjectBean(Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance)) + .computeEdits(params.docUri(), params.typeDeclarationName(), params.type(), params.name())); + } + } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java index 055355d4a8..debb70df2b 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/STS4LanguageClient.java @@ -15,6 +15,8 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.ResourceOperation; +import org.eclipse.lsp4j.TextDocumentEdit; import org.eclipse.lsp4j.jsonrpc.json.ResponseJsonAdapter; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; @@ -22,6 +24,7 @@ import org.eclipse.lsp4j.services.LanguageClient; import org.springframework.ide.vscode.commons.protocol.java.ClasspathListenerParams; import org.springframework.ide.vscode.commons.protocol.java.Gav; +import org.springframework.ide.vscode.commons.protocol.java.InjectBeanParams; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteData; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteParams; import org.springframework.ide.vscode.commons.protocol.java.JavaDataParams; @@ -101,4 +104,7 @@ public interface STS4LanguageClient extends LanguageClient, SpringIndexLanguageC @JsonRequest("sts/project/gav") CompletableFuture> projectGAV(ProjectGavParams params); + @JsonRequest("sts/java/injectBean") + CompletableFuture injectBean(InjectBeanParams params); + } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/InjectBeanParams.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/InjectBeanParams.java new file mode 100644 index 0000000000..4b2202a018 --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/InjectBeanParams.java @@ -0,0 +1,5 @@ +package org.springframework.ide.vscode.commons.protocol.java; + +public record InjectBeanParams(String docUri, String typeDeclarationName, String type, String name) { + +} diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index add53dda8a..15085fdbe1 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -129,6 +129,7 @@ import org.springframework.ide.vscode.commons.protocol.STS4LanguageClient; import org.springframework.ide.vscode.commons.protocol.java.ClasspathListenerParams; import org.springframework.ide.vscode.commons.protocol.java.Gav; +import org.springframework.ide.vscode.commons.protocol.java.InjectBeanParams; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteData; import org.springframework.ide.vscode.commons.protocol.java.JavaCodeCompleteParams; import org.springframework.ide.vscode.commons.protocol.java.JavaDataParams; @@ -486,6 +487,11 @@ public CompletableFuture refreshInlayHints() { // TODO: perhaps at some point the client would need to ask the server for new inlay-hints for opened documents. return CompletableFuture.completedFuture(null); } + + @Override + public CompletableFuture injectBean(InjectBeanParams params) { + return CompletableFuture.completedFuture(null); + } }); } diff --git a/headless-services/spring-boot-language-server/pom.xml b/headless-services/spring-boot-language-server/pom.xml index 211f5aa579..be39c9c186 100644 --- a/headless-services/spring-boot-language-server/pom.xml +++ b/headless-services/spring-boot-language-server/pom.xml @@ -119,6 +119,11 @@ org.eclipse.jdt.core ${jdt.core.version} + + org.eclipse.jdt + org.eclipse.jdt.core.manipulation + 1.21.300 + commons-io diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index c43f0e3cdd..2ce228b550 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -117,7 +117,8 @@ BootJavaCompletionEngine javaCompletionEngine( CompilationUnitCache cuCache, SpringMetamodelIndex springIndex, RewriteRefactorings rewriteRefactorings, - BootJavaConfig config) { + BootJavaConfig config, + SimpleLanguageServer server) { SpringPropertyIndexProvider indexProvider = params.indexProvider; JavaProjectFinder javaProjectFinder = params.projectFinder; @@ -172,7 +173,7 @@ BootJavaCompletionEngine javaCompletionEngine( providers.put(Annotations.SCHEDULED, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of( "cron", new CronExpressionCompletionProvider()))); - providers.put(Annotations.BEAN, new BeanCompletionProvider(javaProjectFinder, springIndex, rewriteRefactorings, config)); + providers.put(Annotations.BEAN, new BeanCompletionProvider(javaProjectFinder, springIndex, rewriteRefactorings, config, cuCache, server)); return new BootJavaCompletionEngine(cuCache, providers, snippetManager); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java index 40ed08c8bd..a64bc22620 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java @@ -11,37 +11,64 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.List; +import java.net.URI; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; import org.apache.commons.text.similarity.JaroWinklerSimilarity; +import org.eclipse.core.runtime.Assert; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Assignment; import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.FieldAccess; +import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.ThisExpression; -import org.eclipse.lsp4j.Command; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.dom.IASTSharedValues; +import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages; +import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; +import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.CompletionItemLabelDetails; +import org.eclipse.lsp4j.ResourceOperation; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.TextEditGroup; import org.openrewrite.java.tree.JavaType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposalWithScore; -import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; -import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; -import org.springframework.ide.vscode.commons.rewrite.java.InjectBeanCompletionRecipe; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.protocol.java.InjectBeanParams; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.text.IDocument; +import com.google.common.base.Suppliers; + /** * @author Udayani V * @author Alex Boyko @@ -65,8 +92,16 @@ public class BeanCompletionProposal implements ICompletionProposalWithScore { private String prefix; private DocumentEdits edits; - public BeanCompletionProposal(ASTNode node, int offset, IDocument doc, String beanId, String beanType, - String fieldName, String className, RewriteRefactorings rewriteRefactorings) { + private IJavaProject project; + + private CompilationUnitCache cuCache; + + private SimpleLanguageServer server; + + public BeanCompletionProposal(SimpleLanguageServer server, IJavaProject project, ASTNode node, int offset, IDocument doc, String beanId, String beanType, + String fieldName, String className, RewriteRefactorings rewriteRefactorings, CompilationUnitCache cuCache) { + this.server = server; + this.project = project; this.node = node; this.offset = offset; this.doc = doc; @@ -75,6 +110,7 @@ public BeanCompletionProposal(ASTNode node, int offset, IDocument doc, String be this.fieldName = fieldName; this.className = className; this.rewriteRefactorings = rewriteRefactorings; + this.cuCache = cuCache; this.prefix = computePrefix(); this.edits = computeEdit(); this.score = /*FuzzyMatcher.matchScore*/computeJaroWinklerScore(prefix, beanId); @@ -133,7 +169,78 @@ private FieldAccess getFieldAccessFromIncompleteThisAssignment(SimpleName sn) { return null; } + private DocumentEdits createFieldDeclaration(CompilationUnit cuRefactorNode) throws JavaModelException, IllegalArgumentException { + CompilationUnit cu = ASTNodes.getParent(node, org.eclipse.jdt.core.dom.CompilationUnit.class); + CompilationUnitRewrite fCuRewrite = new CompilationUnitRewrite(null, (ICompilationUnit) cu.getTypeRoot(), cuRefactorNode, Map.of()); +// Type type= getConstantType(); + + AST ast= fCuRewrite.getAST(); + VariableDeclarationFragment variableDeclarationFragment= ast.newVariableDeclarationFragment(); + variableDeclarationFragment.setName(ast.newSimpleName(fieldName)); + + FieldDeclaration fieldDeclaration= ast.newFieldDeclaration(variableDeclarationFragment); +// fieldDeclaration.setType(type); + fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD)); + fieldDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.FINAL_KEYWORD)); + + AbstractTypeDeclaration parent = ASTNodes.getParent(node, AbstractTypeDeclaration.class); + Assert.isNotNull(parent); + ListRewrite listRewrite= fCuRewrite.getASTRewrite().getListRewrite(parent, parent.getBodyDeclarationsProperty()); + TextEditGroup msg= fCuRewrite.createGroupDescription(RefactoringCoreMessages.ExtractConstantRefactoring_declare_constant); + listRewrite.insertFirst(fieldDeclaration, msg); + + TextEdit edit = fCuRewrite.getASTRewrite().rewriteAST(); + DocumentEdits edits = new DocumentEdits(doc, false); + return edits; + } + private DocumentEdits computeEdit() { +// try { +// CompilationUnit cu = ASTNodes.getParent(node, org.eclipse.jdt.core.dom.CompilationUnit.class); +// RefactoringASTParser parser= new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL); +// +// try { +//// CompilationUnit cuRefactorNode = cuCache.parseCuWithReusableEnv(project, URI.create(doc.getUri())); +// return createFieldDeclaration(cu); +// } catch (JavaModelException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } catch (IllegalArgumentException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +//// } catch (ExecutionException e) { +//// // TODO Auto-generated catch block +//// e.printStackTrace(); +// } catch (Exception e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// return new DocumentEdits(doc, false); + + +// ASTRewrite astRewrite= ASTRewrite.create(cuRefactorNode.getAST()); +// ImportRewrite importRewrite= StubUtility.createImportRewrite(cu, true); +// +// TypeDeclaration declaringType = ASTUtils.findDeclaringType(node); +// +// ITypeBinding currTypeBinding= declaringType.resolveBinding(); +// ListRewrite memberRewriter= null; +// +// ASTNode node= cuRefactorNode.findDeclaringNode(currTypeBinding); +// if (node instanceof AnonymousClassDeclaration) { +// memberRewriter= astRewrite.getListRewrite(node, AnonymousClassDeclaration.BODY_DECLARATIONS_PROPERTY); +// } else if (node instanceof AbstractTypeDeclaration) { +// ChildListPropertyDescriptor property= ((AbstractTypeDeclaration) node).getBodyDeclarationsProperty(); +// memberRewriter= astRewrite.getListRewrite(node, property); +// } else { +// throw new IllegalArgumentException(); +// // not possible, we checked this in the constructor +// } + +// } catch (JavaModelException e) { +// throw new IllegalStateException(e); +// } + DocumentEdits edits = new DocumentEdits(doc, false); if (isInsideConstructor(node)) { if (node instanceof Block) { @@ -179,14 +286,14 @@ public Renderable getDocumentation() { .formatted(beanId, beanType)); } - @Override - public Optional getCommand() { - FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()), - "Inject bean completions") - .withParameters(Map.of("fullyQualifiedName", beanType, "fieldName", fieldName, "classFqName", className)) - .withRecipeScope(RecipeScope.NODE); - return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f)); - } +// @Override +// public Optional getCommand() { +// FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()), +// "Inject bean completions") +// .withParameters(Map.of("fullyQualifiedName", beanType, "fieldName", fieldName, "classFqName", className)) +// .withRecipeScope(RecipeScope.NODE); +// return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f)); +// } @Override public double getScore() { @@ -202,6 +309,39 @@ private boolean isInsideConstructor(ASTNode node) { return false; } + @Override + public Optional> getAdditionalEdit() { + return Optional.of(Suppliers.memoize(() -> { + long start = System.currentTimeMillis(); + DocumentEdits additionalEdit = new DocumentEdits(doc, false); + try { + TextDocumentEdit beanInjectEdits = server.getClient().injectBean(new InjectBeanParams(doc.getUri(), + JavaType.ShallowClass.build(className).getClassName(), beanType, fieldName)).get(); + if (beanInjectEdits != null) { + for (org.eclipse.lsp4j.TextEdit e : beanInjectEdits.getEdits()) { + try { + int startLineOffset = doc.getLineOffset(e.getRange().getStart().getLine() - 1); + if (!e.getRange().getEnd().equals(e.getRange().getStart())) { + int endLineOffset = doc.getLineOffset(e.getRange().getEnd().getLine() - 1); + additionalEdit.delete(startLineOffset + e.getRange().getStart().getCharacter() + 1, endLineOffset + e.getRange().getEnd().getCharacter() + 1); + } + if (!e.getNewText().isEmpty()) { + additionalEdit.insert(startLineOffset + e.getRange().getStart().getCharacter() + 1 + , e.getNewText()); + } + } catch (BadLocationException ex) { + log.error("Failed to compute edit", ex); + } + } + } + } catch (Exception e) { + log.error("Failed to fetch edits for Java LS", e); + } + log.info("ADDITIONAL EDIT %d".formatted(System.currentTimeMillis() - start)); + return additionalEdit; + })); + } + @Override public int hashCode() { return Objects.hash(beanId, beanType); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java index 30e7e31fc4..6906c790db 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java @@ -36,9 +36,11 @@ import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -55,12 +57,18 @@ public class BeanCompletionProvider implements CompletionProvider { private final BootJavaConfig config; + private CompilationUnitCache cuCache; + + private SimpleLanguageServer server; + public BeanCompletionProvider(JavaProjectFinder javaProjectFinder, SpringMetamodelIndex springIndex, - RewriteRefactorings rewriteRefactorings, BootJavaConfig config) { + RewriteRefactorings rewriteRefactorings, BootJavaConfig config, CompilationUnitCache cuCache, SimpleLanguageServer server) { this.javaProjectFinder = javaProjectFinder; this.springIndex = springIndex; this.rewriteRefactorings = rewriteRefactorings; this.config = config; + this.cuCache = cuCache; + this.server = server; } @Override @@ -134,8 +142,8 @@ public void provideCompletions(ASTNode node, int offset, TextDocument doc, Colle for (int i = 0; i < Integer.MAX_VALUE && fieldNames.contains(fieldName); i++, fieldName = "%s_%d".formatted(bean.getName(), i)) { // nothing } - BeanCompletionProposal proposal = new BeanCompletionProposal(node, offset, doc, bean.getName(), - bean.getType(), fieldName, className, rewriteRefactorings); + BeanCompletionProposal proposal = new BeanCompletionProposal(server, project, node, offset, doc, bean.getName(), + bean.getType(), fieldName, className, rewriteRefactorings, cuCache); if (proposal.getScore() > 0) { beanCompletions.add(proposal); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 58ce9fd5f3..4d5284490a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -305,6 +305,20 @@ private synchronized CompletableFuture requestCU(IJavaProject p } return cuFuture; } + + public CompilationUnit parseCuWithReusableEnv(IJavaProject project, URI uri) throws ExecutionException, Exception { + logger.info("Started parsing CU for " + uri); + Tuple2, INameEnvironmentWithProgress> lookupEnvTuple = loadLookupEnvTuple(project); + String uriStr = uri.toASCIIString(); + String unitName = uriStr.substring(uriStr.lastIndexOf("/") + 1); // skip over '/' + CompilationUnit cUnit = parse2(fetchContent(uri).toCharArray(), uriStr, unitName, lookupEnvTuple.getT1(), lookupEnvTuple.getT2(), + annotationHierarchies.get(project.getLocationUri(), AnnotationHierarchies::new)); + + logger.debug("CU Cache: created new AST for {}", uri.toASCIIString()); + + logger.info("Parsed successfully CU for " + uri); + return cUnit; + } public static CompilationUnit parse2(char[] source, String docURI, String unitName, IJavaProject project) throws Exception { List classpaths = createClasspath(getClasspathEntries(project));