Inject Bean completion proposal via Rewrite recipes

This commit is contained in:
vudayani
2024-10-03 11:27:30 +05:30
committed by aboyko
parent ca1097058f
commit 41fa3f4a12
20 changed files with 2382 additions and 19 deletions

View File

@@ -26,6 +26,7 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeC
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.beans.BeanNamesCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.BeanTypesCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.BeanCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
@@ -38,6 +39,7 @@ import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionPro
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
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.scope.ScopeCompletionProcessor;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext;
@@ -113,7 +115,8 @@ public class BootJavaCompletionEngineConfigurer {
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
JavaSnippetManager snippetManager,
CompilationUnitCache cuCache,
SpringMetamodelIndex springIndex) {
SpringMetamodelIndex springIndex,
RewriteRefactorings rewriteRefactorings ) {
SpringPropertyIndexProvider indexProvider = params.indexProvider;
JavaProjectFinder javaProjectFinder = params.projectFinder;
@@ -168,6 +171,8 @@ public class BootJavaCompletionEngineConfigurer {
providers.put(Annotations.SCHEDULED, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of(
"cron", new CronExpressionCompletionProvider())));
providers.put(Annotations.BEAN, new BeanCompletionProvider(javaProjectFinder, springIndex, rewriteRefactorings));
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
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.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* @author Udayani V
* @author Alex Boyko
*/
public class BeanCompletionProposal implements ICompletionProposal {
private DocumentEdits edits;
private IDocument doc;
private String beanId;
private String beanType;
private String className;
private RewriteRefactorings rewriteRefactorings;
public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String beanId, String beanType, String className,
RewriteRefactorings rewriteRefactorings) {
this.edits = edits;
this.doc = doc;
this.beanId = beanId;
this.beanType = beanType;
this.className = className;
this.rewriteRefactorings = rewriteRefactorings;
}
@Override
public String getLabel() {
return this.beanId;
}
@Override
public CompletionItemKind getKind() {
return CompletionItemKind.Constructor;
}
@Override
public DocumentEdits getTextEdit() {
return edits;
}
@Override
public String getDetail() {
return "Autowire a bean";
}
@Override
public Renderable getDocumentation() {
return Renderables.text(
"Inject bean `%s` of type `%s` as a constructor parameter and add corresponding field".formatted(beanId, beanType));
}
@Override
public Optional<Command> getCommand() {
FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()),"Inject bean completions")
.withParameters(Map.of("fullyQualifiedName", beanType, "fieldName", beanId, "classFqName", className))
.withRecipeScope(RecipeScope.NODE);
return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f));
}
}

View File

@@ -0,0 +1,143 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Collection;
import java.util.Optional;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
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.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Udayani V
*/
public class BeanCompletionProvider implements CompletionProvider {
private static final Logger log = LoggerFactory.getLogger(BeanCompletionProvider.class);
private final JavaProjectFinder javaProjectFinder;
private final SpringMetamodelIndex springIndex;
private final RewriteRefactorings rewriteRefactorings;
public BeanCompletionProvider(JavaProjectFinder javaProjectFinder, SpringMetamodelIndex springIndex,
RewriteRefactorings rewriteRefactorings) {
this.javaProjectFinder = javaProjectFinder;
this.springIndex = springIndex;
this.rewriteRefactorings = rewriteRefactorings;
}
@Override
public void provideCompletions(ASTNode node, int offset, TextDocument doc,
Collection<ICompletionProposal> completions) {
if (node instanceof SimpleName) {
try {
// Don't look at anything inside Annotation or VariableDelcaration node
for (ASTNode n = node; n != null; n = n.getParent()) {
if (n instanceof Annotation
|| n instanceof VariableDeclaration) {
return;
}
}
Optional<IJavaProject> optionalProject = this.javaProjectFinder.find(doc.getId());
if (optionalProject.isEmpty()) {
return;
}
IJavaProject project = optionalProject.get();
TypeDeclaration topLevelClass = findParentClass(node);
if (topLevelClass == null) {
return;
}
if (isSpringComponent(topLevelClass)) {
String className = getFullyQualifiedName(topLevelClass);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
for (Bean bean : beans) {
if (FuzzyMatcher.matchScore(node.toString(), bean.getName()) != 0.0) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset - node.toString().length(), offset, bean.getName());
BeanCompletionProposal proposal = new BeanCompletionProposal(edits, doc, bean.getName(),
bean.getType(), className, rewriteRefactorings);
completions.add(proposal);
}
}
}
} catch (Exception e) {
log.error("problem while looking for bean completions", e);
}
}
}
private static boolean isSpringComponent(TypeDeclaration node) {
for (IAnnotationBinding annotation : node.resolveBinding().getAnnotations()) {
if (isSpringComponentAnnotation(annotation)) {
return true;
}
}
return false;
}
private static boolean isSpringComponentAnnotation(IAnnotationBinding annotation) {
String annotationName = annotation.getAnnotationType().getQualifiedName();
if (annotationName.equals("org.springframework.stereotype.Component")) {
return true;
}
for (IAnnotationBinding metaAnnotation : annotation.getAnnotationType().getAnnotations()) {
if (metaAnnotation.getAnnotationType().getQualifiedName().equals("org.springframework.stereotype.Component")) {
return true;
}
}
return false;
}
private static TypeDeclaration findParentClass(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof TypeDeclaration) {
return (TypeDeclaration) current;
}
current = current.getParent();
}
return null;
}
private static String getFullyQualifiedName(TypeDeclaration typeDecl) {
if (typeDecl.resolveBinding() != null) {
String qualifiedName = typeDecl.resolveBinding().getQualifiedName();
return qualifiedName.replaceAll("\\.(?=[^\\.]+$)", "\\$");
}
CompilationUnit cu = (CompilationUnit) typeDecl.getRoot();
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
String typeName = typeDecl.getName().getFullyQualifiedName();
return packageName.isEmpty() ? typeName : packageName + "." + typeName;
}
}

View File

@@ -15,6 +15,7 @@ import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
@@ -22,11 +23,13 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.openrewrite.Parser.Input;
import org.openrewrite.Recipe;
import org.openrewrite.SourceFile;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.openrewrite.java.JavaParser;
import org.openrewrite.marker.Range;
@@ -82,12 +85,23 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
@Override
public Mono<QuickfixEdit> createEdits(Object p) {
if (p instanceof JsonElement) {
return Mono.fromFuture(createEdit((JsonElement) p).thenApply(we -> new QuickfixEdit(we, null)));
if (p instanceof JsonElement je) {
return Mono.fromFuture(createEdit(je).thenApply(we -> new QuickfixEdit(we, null)));
} else {
return Mono.fromFuture(createEdit(gson.toJsonTree(p)).thenApply(we -> new QuickfixEdit(we, null)));
}
return null;
}
public Command createFixCommand(String title, FixDescriptor f) {
List<Object> args = new ArrayList<>(3);
args.add(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX);
args.add(gson.toJsonTree(f));
return new Command(
title,
server.CODE_ACTION_COMMAND_ID,
args
);
}
@Override
public CompletableFuture<WorkspaceEdit> resolve(CodeAction codeAction) {
@@ -154,16 +168,8 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
.orElseGet(() -> recipeRepo.getRecipe(d.getRecipeId()).thenApply(opt -> opt.orElseThrow())))
.thenApply(r -> {
if (d.getParameters() != null) {
for (Entry<String, Object> entry : d.getParameters().entrySet()) {
try {
Field f = r.getClass().getDeclaredField(entry.getKey());
f.setAccessible(true);
f.set(r, entry.getValue());
} catch (Exception e) {
log.error("", e);;
}
}
}
setParameters(r, d.getParameters());
}
if (d.getRecipeScope() == RecipeScope.NODE) {
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
@@ -187,4 +193,41 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
return r;
});
}
/**
* Sets the parameters for a given recipe. If the recipe is a DeclarativeRecipe,
* it iterates over its sub-recipes and sets the parameters for each sub-recipe.
*/
private void setParameters(Recipe recipe, Map<String, Object> parameters) {
if (recipe instanceof DeclarativeRecipe) {
List<Recipe> subRecipes = ((DeclarativeRecipe) recipe).getRecipeList();
for (Recipe subRecipe : subRecipes) {
setParameters(subRecipe, parameters);
}
} else {
for (Entry<String, Object> entry : parameters.entrySet()) {
try {
Field field = findField(recipe, entry.getKey());
if (field != null) {
field.setAccessible(true);
field.set(recipe, entry.getValue());
}
} catch (Exception e) {
log.error("", e);;
}
}
}
}
private Field findField(Object obj, String fieldName) {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}

View File

@@ -0,0 +1,362 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Udayani V
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class BeanCompletionProviderTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringMetamodelIndex springIndex;
@Autowired private SpringSymbolIndex indexer;
private File directory;
private IJavaProject project;
private Bean[] indexedBeans;
private String tempJavaDocUri;
private Bean bean1;
private Bean bean2;
private Bean bean3;
private Bean bean4;
private Bean bean5;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
String projectDir = directory.toURI().toString();
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
indexedBeans = springIndex.getBeansOfProject(project.getElementName());
tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
bean1 = new Bean("ownerRepository", "org.springframework.samples.petclinic.owner.OwnerRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean2 = new Bean("ownerService", "org.springframework.samples.petclinic.owner.OwnerService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean3 = new Bean("visitRepository", "org.springframework.samples.petclinic.owner.VisitRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean4 = new Bean("visitService", "org.springframework.samples.petclinic.owner.VisitService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
bean5 = new Bean("petService", "org.springframework.samples.petclinic.pet.Inner.PetService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false);
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5});
}
@AfterEach
public void restoreIndexState() {
this.springIndex.updateBeans(project.getElementName(), indexedBeans);
}
@Test
public void testBeanCompletion_withMatches() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
ownerRepository<*>
}
}
""");
}
@Test
public void testBeanCompletion_withoutMatches() throws Exception {
assertCompletions(getCompletion("rand<*>"), new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_chooseSecondCompletion() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerService ownerService;
TestBeanCompletionClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void testBeanCompletion_injectInnerClass() throws Exception {
assertCompletions(getCompletion("pet<*>"), new String[] {"petService"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.pet.Inner.PetService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final Inner.PetService petService;
TestBeanCompletionClass(Inner.PetService petService) {
this.petService = petService;
}
public void test() {
petService<*>
}
}
""");
}
@Test
public void testBeanCompletion_multipleClasses() throws Exception {
String content = """
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
public void test() {
owner<*>
}
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
private final OwnerService ownerService;
TestBeanCompletionSecondClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void testBeanCompletion_isNotSpringComponent() throws Exception {
String content = """
package org.sample.test;
public class TestBeanCompletionClass {
public void test() {
owner<*>
}
}
""";
// No suggestions when it is not a spring component
assertCompletions(content, new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_isOutsideMethod() throws Exception {
String content = """
package org.sample.test;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
owner<*>
}
""";
assertCompletions(content, new String[] {}, 0, "");
}
@Test
public void testBeanCompletion_nestedComponent() throws Exception {
String content = """
package org.sample.test;
import org.springframework.stereotype.Component;
@Component
public class TestBeanCompletionClass {
@Component
public class Inner {
public void test() {
ownerRe<*>
}
}
}
""";
assertCompletions(content, new String[] {"ownerRepository"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Component;
@Component
public class TestBeanCompletionClass {
@Component
public class Inner {
private final OwnerRepository ownerRepository;
Inner(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
ownerRepository<*>
}
}
}
""");
}
private String getCompletion(String completionLine) {
String content = """
package org.sample.test;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
public void test() {
""" +
completionLine + "\n" +
"""
}
}
""";
return content;
}
private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedResult);
}
private void assertCompletions(String editorContent, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
List<CompletionItem> completions = editor.getCompletions();
assertEquals(noOfExcpectedCompletions, completions.size());
if (expectedCompletions != null) {
String[] completionItems = completions.stream()
.map(item -> item.getLabel())
.toArray(size -> new String[size]);
assertArrayEquals(expectedCompletions, completionItems);
}
if (noOfExcpectedCompletions > 0) {
editor.apply(completions.get(chosenCompletion));
assertEquals(expectedResult, editor.getText());
}
}
}