From 5b434b16c49deb2d2580ce8fdd70cb65b5cb35e2 Mon Sep 17 00:00:00 2001 From: nsingh Date: Thu, 28 Sep 2017 16:08:25 -0700 Subject: [PATCH] Added JDT-derived import rewrite for snippets --- .../handlers/BootJavaCompletionEngine.java | 2 +- .../boot/java/jdt/imports/ImportRewrite.java | 558 ++++++++++++++++++ .../boot/java/snippets/JavaSnippet.java | 8 +- .../java/snippets/JavaSnippetCompletion.java | 21 +- .../java/snippets/JavaSnippetManager.java | 5 +- 5 files changed, 584 insertions(+), 10 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/imports/ImportRewrite.java diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java index 82295b53a..f47bd2042 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java @@ -75,7 +75,7 @@ public class BootJavaCompletionEngine implements ICompletionEngine { System.out.println("AST node found: " + node.getClass().getName()); Collection completions = new ArrayList(); completions.addAll(collectCompletionsForAnnotations(node, offset, document)); - completions.addAll(snippets.getCompletions(document, offset, node)); + completions.addAll(snippets.getCompletions(document, offset, node, cu)); return completions; } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/imports/ImportRewrite.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/imports/ImportRewrite.java new file mode 100644 index 000000000..7cb23c656 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/imports/ImportRewrite.java @@ -0,0 +1,558 @@ +/******************************************************************************* + * Derived from: + * org.eclipse.jdt.core.dom.rewrite.ImportRewrite + * + * for use in STS4, where IProject and ICompilationUnit are not available when parsing a Java source. + * + * Original license: + * + * Copyright (c) 2000, 2016 IBM Corporation and others. + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + * Lars Vogel - Contributions for + * Bug 473178 + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.jdt.imports; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.compiler.CharOperation; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.Comment; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.PackageDeclaration; +import org.eclipse.jdt.core.dom.PrimitiveType; +import org.eclipse.jdt.core.dom.SimpleName; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.util.text.IDocument; + + +/** + * The {@link ImportRewrite} helps updating imports following a import order and on-demand imports threshold as configured by a project. + *

+ * The import rewrite is created on a compilation unit and collects references to types that are added or removed. When adding imports, e.g. using + * {@link #addImport(String)}, the import rewrite evaluates if the type can be imported and returns the a reference to the type that can be used in code. + * This reference is either unqualified if the import could be added, or fully qualified if the import failed due to a conflict with another element of the same name. + *

+ *

+ * On {@link #rewriteImports(IProgressMonitor)} the rewrite translates these descriptions into + * text edits that can then be applied to the original source. The rewrite infrastructure tries to generate minimal text changes and only + * works on the import statements. It is possible to combine the result of an import rewrite with the result of a {@link org.eclipse.jdt.core.dom.rewrite.ASTRewrite} + * as long as no import statements are modified by the AST rewrite. + *

+ *

The options controlling the import order and on-demand thresholds are: + *

  • {@link #setImportOrder(String[])} specifies the import groups and their preferred order
  • + *
  • {@link #setOnDemandImportThreshold(int)} specifies the number of imports in a group needed for a on-demand import statement (star import)
  • + *
  • {@link #setStaticOnDemandImportThreshold(int)} specifies the number of static imports in a group needed for a on-demand import statement (star import)
  • + *
+ * This class is not intended to be subclassed. + *

+ * @since 3.2 + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public final class ImportRewrite { + + /** + * A {@link ImportRewrite.ImportRewriteContext} can optionally be used in e.g. {@link ImportRewrite#addImport(String, ImportRewrite.ImportRewriteContext)} to + * give more information about the types visible in the scope. These types can be for example inherited inner types where it is + * unnecessary to add import statements for. + * + *

+ *

+ * This class can be implemented by clients. + *

+ */ + public static abstract class ImportRewriteContext { + + /** + * Result constant signaling that the given element is know in the context. + */ + public final static int RES_NAME_FOUND= 1; + + /** + * Result constant signaling that the given element is not know in the context. + */ + public final static int RES_NAME_UNKNOWN= 2; + + /** + * Result constant signaling that the given element is conflicting with an other element in the context. + */ + public final static int RES_NAME_CONFLICT= 3; + + /** + * Result constant signaling that the given element must be imported explicitly (and must not be folded into + * an on-demand import or filtered as an implicit import). + * + * @since 3.11 + */ + public final static int RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT= 4; + + /** + * Kind constant specifying that the element is a type import. + */ + public final static int KIND_TYPE= 1; + + /** + * Kind constant specifying that the element is a static field import. + */ + public final static int KIND_STATIC_FIELD= 2; + + /** + * Kind constant specifying that the element is a static method import. + */ + public final static int KIND_STATIC_METHOD= 3; + + /** + * Searches for the given element in the context and reports if the element is known ({@link #RES_NAME_FOUND}), + * unknown ({@link #RES_NAME_UNKNOWN}), unknown in the context but known to require an explicit import + * ({@link #RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT}), or if its name conflicts ({@link #RES_NAME_CONFLICT}) + * with an other element. + * + * @param qualifier The qualifier of the element, can be package or the qualified name of a type + * @param name The simple name of the element; either a type, method or field name or * for on-demand imports. + * @param kind The kind of the element. Can be either {@link #KIND_TYPE}, {@link #KIND_STATIC_FIELD} or + * {@link #KIND_STATIC_METHOD}. Implementors should be prepared for new, currently unspecified kinds and return + * {@link #RES_NAME_UNKNOWN} by default. + * @return Returns the result of the lookup. Can be either {@link #RES_NAME_FOUND}, {@link #RES_NAME_UNKNOWN}, + * {@link #RES_NAME_CONFLICT}, or {@link #RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT}. + */ + public abstract int findInContext(String qualifier, String name, int kind); + } + + private static final char STATIC_PREFIX= 's'; + private static final char NORMAL_PREFIX= 'n'; + + private final ImportRewriteContext defaultContext; + + private final CompilationUnit astRoot; + + private final boolean restoreExistingImports; + private final List existingImports; + + + private List addedImports; + + /** + * Simple names of non-static imports which must not be reduced into on-demand imports + * or filtered out as implicit. + */ + private Set typeExplicitSimpleNames; + + + private boolean filterImplicitImports; + private boolean useContextToFilterImplicitImports; + + + /** + * Creates an {@link ImportRewrite} from an AST ({@link CompilationUnit}). The AST has to be created from an + * {@link ICompilationUnit}, that means {@link ASTParser#setSource(ICompilationUnit)} has been used when creating the + * AST. If restoreExistingImports is true, all existing imports are kept, and new imports + * will be inserted at best matching locations. If restoreExistingImports is false, the + * existing imports will be removed and only the newly added imports will be created. + *

+ * Note that this method is more efficient than using {@link #create(ICompilationUnit, boolean)} if an AST is already available. + *

+ * @param astRoot the AST root node to create the imports for + * @param restoreExistingImports specifies if the existing imports should be kept or removed. + * @return the created import rewriter. + * @throws IllegalArgumentException thrown when the passed AST is null or was not created from a compilation unit. + */ + public static ImportRewrite create(CompilationUnit astRoot, boolean restoreExistingImports) { + if (astRoot == null) { + throw new IllegalArgumentException("AST must not be null"); //$NON-NLS-1$ + } + + List existingImport= null; + if (restoreExistingImports) { + existingImport= new ArrayList(); + List imports= astRoot.imports(); + for (int i= 0; i < imports.size(); i++) { + ImportDeclaration curr= (ImportDeclaration) imports.get(i); + StringBuffer buf= new StringBuffer(); + buf.append(curr.isStatic() ? STATIC_PREFIX : NORMAL_PREFIX).append(curr.getName().getFullyQualifiedName()); + if (curr.isOnDemand()) { + if (buf.length() > 1) + buf.append('.'); + buf.append('*'); + } + existingImport.add(buf.toString()); + } + } + return new ImportRewrite(astRoot, existingImport); + } + + private ImportRewrite(CompilationUnit astRoot, List existingImports) { + this.astRoot= astRoot; // might be null + if (existingImports != null) { + this.existingImports= existingImports; + this.restoreExistingImports= !existingImports.isEmpty(); + } else { + this.existingImports= new ArrayList(); + this.restoreExistingImports= false; + } + this.filterImplicitImports= true; + // consider that no contexts are used + this.useContextToFilterImplicitImports = false; + + this.defaultContext= new ImportRewriteContext() { + @Override + public int findInContext(String qualifier, String name, int kind) { + return findInImports(qualifier, name, kind); + } + }; + this.addedImports= new ArrayList<>(); + this.typeExplicitSimpleNames = new HashSet<>(); + } + + /** + * Returns the default rewrite context that only knows about the imported types. Clients + * can write their own context and use the default context for the default behavior. + * @return the default import rewrite context. + */ + public ImportRewriteContext getDefaultImportRewriteContext() { + return this.defaultContext; + } + + /** + * Specifies that implicit imports (for types in java.lang, types in the same package as the rewrite + * compilation unit, and types in the compilation unit's main type) should not be created, except if necessary to + * resolve an on-demand import conflict. + *

+ * The filter is enabled by default. + *

+ *

+ * Note: {@link #setUseContextToFilterImplicitImports(boolean)} can be used to filter implicit imports + * when a context is used. + *

+ * + * @param filterImplicitImports + * if true, implicit imports will be filtered + * + * @see #setUseContextToFilterImplicitImports(boolean) + */ + public void setFilterImplicitImports(boolean filterImplicitImports) { + this.filterImplicitImports= filterImplicitImports; + } + + /** + * Sets whether a context should be used to properly filter implicit imports. + *

+ * By default, the option is disabled to preserve pre-3.6 behavior. + *

+ *

+ * When this option is set, the context passed to the addImport*(...) methods is used to determine + * whether an import can be filtered because the type is implicitly visible. Note that too many imports + * may be kept if this option is set and addImport*(...) methods are called without a context. + *

+ * + * @param useContextToFilterImplicitImports the given setting + * + * @see #setFilterImplicitImports(boolean) + * @since 3.6 + */ + public void setUseContextToFilterImplicitImports(boolean useContextToFilterImplicitImports) { + this.useContextToFilterImplicitImports = useContextToFilterImplicitImports; + } + + private static int compareImport(char prefix, String qualifier, String name, String curr) { + if (curr.charAt(0) != prefix || !curr.endsWith(name)) { + return ImportRewriteContext.RES_NAME_UNKNOWN; + } + + curr= curr.substring(1); // remove the prefix + + if (curr.length() == name.length()) { + if (qualifier.length() == 0) { + return ImportRewriteContext.RES_NAME_FOUND; + } + return ImportRewriteContext.RES_NAME_CONFLICT; + } + // at this place: curr.length > name.length + + int dotPos= curr.length() - name.length() - 1; + if (curr.charAt(dotPos) != '.') { + return ImportRewriteContext.RES_NAME_UNKNOWN; + } + if (qualifier.length() != dotPos || !curr.startsWith(qualifier)) { + return ImportRewriteContext.RES_NAME_CONFLICT; + } + return ImportRewriteContext.RES_NAME_FOUND; + } + + /** + * Not API, package visibility as accessed from an anonymous type + */ + /* package */ final int findInImports(String qualifier, String name, int kind) { + boolean allowAmbiguity= (kind == ImportRewriteContext.KIND_STATIC_METHOD) || (name.length() == 1 && name.charAt(0) == '*'); + List imports= this.existingImports; + char prefix= (kind == ImportRewriteContext.KIND_TYPE) ? NORMAL_PREFIX : STATIC_PREFIX; + + for (int i= imports.size() - 1; i >= 0 ; i--) { + String curr= (String) imports.get(i); + int res= compareImport(prefix, qualifier, name, curr); + if (res != ImportRewriteContext.RES_NAME_UNKNOWN) { + if (!allowAmbiguity || res == ImportRewriteContext.RES_NAME_FOUND) { + if (prefix != STATIC_PREFIX) { + return res; + } + } + } + } + + String packageName = getPackageName(); + if (kind == ImportRewriteContext.KIND_TYPE) { + if (this.filterImplicitImports && this.useContextToFilterImplicitImports) { + + // [STS4] No ICompilationUnit available as there is no class file or associated IJavaElement available for the source + +// String mainTypeSimpleName= JavaCore.removeJavaLikeExtension(this.compilationUnit.getElementName()); +// String mainTypeName= Util.concatenateName(packageName, mainTypeSimpleName, '.'); +// if (qualifier.equals(packageName) +// || mainTypeName.equals(Util.concatenateName(qualifier, name, '.'))) { +// return ImportRewriteContext.RES_NAME_FOUND; +// } + + if (this.astRoot != null) { + List types = this.astRoot.types(); + int nTypes = types.size(); + for (int i = 0; i < nTypes; i++) { + AbstractTypeDeclaration type = types.get(i); + SimpleName simpleName = type.getName(); + if (simpleName.getIdentifier().equals(name)) { + return qualifier.equals(packageName) + ? ImportRewriteContext.RES_NAME_FOUND + : ImportRewriteContext.RES_NAME_CONFLICT; + } + } + } else { + + // [STS4] No ICompilationUnit available as there is no class file or associated IJavaElement available for the source +// try { +// IType[] types = this.compilationUnit.getTypes(); +// int nTypes = types.length; +// for (int i = 0; i < nTypes; i++) { +// IType type = types[i]; +// String typeName = type.getElementName(); +// if (typeName.equals(name)) { +// return qualifier.equals(packageName) +// ? ImportRewriteContext.RES_NAME_FOUND +// : ImportRewriteContext.RES_NAME_CONFLICT; +// } +// } +// } catch (JavaModelException e) { +// // don't want to throw an exception here +// } + } + } + } + + return ImportRewriteContext.RES_NAME_UNKNOWN; + } + + private String getPackageName() { + // [STS4] No ICompilationUnit available as there is no class file or associated IJavaElement available for the source +// this.compilationUnit.getParent().getElementName(); + return this.astRoot.getPackage().getName().getFullyQualifiedName(); + } + + /** + * Adds a new import to the rewriter's record and returns a type reference that can be used + * in the code. The type binding can only be an array or non-generic type. + *

+ * No imports are added for types that are already known. If a import for a type is recorded to be removed, this record is discarded instead. + *

+ *

+ * The content of the compilation unit itself is actually not modified + * in any way by this method; rather, the rewriter just records that a new import has been added. + *

+ * @param qualifiedTypeName the qualified type name of the type to be added + * @param context an optional context that knows about types visible in the current scope or null + * to use the default context only using the available imports. + * @return a type reference for the given qualified type name. The type name is a simple name if an import could be used, + * or else a qualified name if an import conflict prevented an import. + */ + public String addImport(String qualifiedTypeName, ImportRewriteContext context) { + int angleBracketOffset= qualifiedTypeName.indexOf('<'); + if (angleBracketOffset != -1) { + return internalAddImport(qualifiedTypeName.substring(0, angleBracketOffset), context) + qualifiedTypeName.substring(angleBracketOffset); + } + int bracketOffset= qualifiedTypeName.indexOf('['); + if (bracketOffset != -1) { + return internalAddImport(qualifiedTypeName.substring(0, bracketOffset), context) + qualifiedTypeName.substring(bracketOffset); + } + return internalAddImport(qualifiedTypeName, context); + } + + /** + * Adds a new import to the rewriter's record and returns a type reference that can be used + * in the code. The type binding can only be an array or non-generic type. + *

+ * No imports are added for types that are already known. If a import for a type is recorded to be removed, this record is discarded instead. + *

+ *

+ * The content of the compilation unit itself is actually not modified + * in any way by this method; rather, the rewriter just records that a new import has been added. + *

+ * @param qualifiedTypeName the qualified type name of the type to be added + * @return a type reference for the given qualified type name. The type name is a simple name if an import could be used, + * or else a qualified name if an import conflict prevented an import. + */ + public String addImport(String qualifiedTypeName) { + return addImport(qualifiedTypeName, this.defaultContext); + } + + + + private String internalAddImport(String fullTypeName, ImportRewriteContext context) { + int idx= fullTypeName.lastIndexOf('.'); + String typeContainerName, typeName; + if (idx != -1) { + typeContainerName= fullTypeName.substring(0, idx); + typeName= fullTypeName.substring(idx + 1); + } else { + typeContainerName= ""; //$NON-NLS-1$ + typeName= fullTypeName; + } + + if (typeContainerName.length() == 0 && PrimitiveType.toCode(typeName) != null) { + return fullTypeName; + } + + if (context == null) + context= this.defaultContext; + + int res= context.findInContext(typeContainerName, typeName, ImportRewriteContext.KIND_TYPE); + if (res == ImportRewriteContext.RES_NAME_CONFLICT) { + return fullTypeName; + } + if (res == ImportRewriteContext.RES_NAME_UNKNOWN) { + addEntry(NORMAL_PREFIX + fullTypeName); + } + if (res == ImportRewriteContext.RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT) { + addEntry(NORMAL_PREFIX + fullTypeName); + this.typeExplicitSimpleNames.add(typeName); + } + return typeName; + } + + private void addEntry(String entry) { + this.existingImports.add(entry); + + this.addedImports.add(entry); + } + + + + /** + * Returns all non-static imports that are recorded to be added. + * + * @return the imports recorded to be added. + */ + public String[] getAddedImports() { + return filterFromList(this.addedImports, NORMAL_PREFIX); + } + + /** + * Returns true if imports have been recorded to be added or removed. + * @return boolean returns if any changes to imports have been recorded. + */ + public boolean hasRecordedChanges() { + return !this.restoreExistingImports + || !this.addedImports.isEmpty(); + } + + private static String[] filterFromList(List imports, char prefix) { + if (imports == null) { + return CharOperation.NO_STRINGS; + } + List res= new ArrayList<>(); + for (String curr : imports) { + if (prefix == curr.charAt(0)) { + res.add(curr.substring(1)); + } + } + return res.toArray(new String[res.size()]); + } + + + /** + * Reads the positions of each existing import declaration along with any associated comments, + * and returns these in a list whose iteration order reflects the existing order of the imports + * in the compilation unit. + */ + private int getAddedImportsInsertLocation() { + List importDeclarations = astRoot.imports(); + + if (importDeclarations == null) { + importDeclarations = Collections.emptyList(); + } + + List comments = astRoot.getCommentList(); + + int currentCommentIndex = 0; + + // Skip over package and file header comments (see https://bugs.eclipse.org/121428). + ImportDeclaration firstImport = importDeclarations.get(0); + PackageDeclaration packageDeclaration = astRoot.getPackage(); + int firstImportStartPosition = packageDeclaration == null + ? firstImport.getStartPosition() + : astRoot.getExtendedStartPosition(packageDeclaration) + + astRoot.getExtendedLength(packageDeclaration); + while (currentCommentIndex < comments.size() + && comments.get(currentCommentIndex).getStartPosition() < firstImportStartPosition) { + currentCommentIndex++; + } + + int previousExtendedEndPosition = -1; + for (ImportDeclaration currentImport : importDeclarations) { + int extendedEndPosition = astRoot.getExtendedStartPosition(currentImport) + + astRoot.getExtendedLength(currentImport); + + int commentAfterImportIndex = currentCommentIndex; + while (commentAfterImportIndex < comments.size() + && comments.get(commentAfterImportIndex).getStartPosition() < extendedEndPosition) { + commentAfterImportIndex++; + } + + + currentCommentIndex = commentAfterImportIndex; + previousExtendedEndPosition = extendedEndPosition; + } + + return previousExtendedEndPosition; + } + + public DocumentEdits createEdit(IDocument doc) { + DocumentEdits edits = null; + StringBuffer buffer = new StringBuffer(); + + String[] createdImprts = getAddedImports(); + if (createdImprts != null && createdImprts.length >0) { + edits =new DocumentEdits(doc); + buffer.append('\n'); + for (String imp : createdImprts) { + buffer.append("import "); + buffer.append(imp); + buffer.append(';'); + buffer.append('\n'); + } + edits.insert(getAddedImportsInsertLocation(), buffer.toString()); + + } + return edits; + } +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippet.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippet.java index e5da5175c..0e3fa7755 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippet.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippet.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; @@ -44,12 +45,13 @@ public class JavaSnippet { } public Optional generateCompletion(Supplier snippetBuilderFactory, - DocumentRegion query, ASTNode node) { + DocumentRegion query, ASTNode node, CompilationUnit cu) { if (context.appliesTo(node)) { return Optional.of( new JavaSnippetCompletion(snippetBuilderFactory, query, + cu, this ) ); @@ -66,8 +68,8 @@ public class JavaSnippet { return this.template; } - public List getImports() { - return this.imports; + public Optional> getImports() { + return Optional.of(this.imports); } public CompletionItemKind getKind() { diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetCompletion.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetCompletion.java index 266105a05..0be2d8366 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetCompletion.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetCompletion.java @@ -12,7 +12,9 @@ package org.springframework.ide.vscode.boot.java.snippets; import java.util.Optional; +import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.boot.java.jdt.imports.ImportRewrite; 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.util.DocumentRegion; @@ -27,10 +29,12 @@ public class JavaSnippetCompletion implements ICompletionProposal{ private DocumentRegion query; private JavaSnippet javaSnippet; private Supplier snippetBuilderFactory; + private CompilationUnit cu; - public JavaSnippetCompletion(Supplier snippetBuilderFactory, DocumentRegion query, JavaSnippet javaSnippet) { + public JavaSnippetCompletion(Supplier snippetBuilderFactory, DocumentRegion query, CompilationUnit cu, JavaSnippet javaSnippet) { this.snippetBuilderFactory = snippetBuilderFactory; this.query = query; + this.cu = cu; this.javaSnippet = javaSnippet; } @@ -61,8 +65,17 @@ public class JavaSnippetCompletion implements ICompletionProposal{ @Override public Optional getAdditionalEdit() { - DocumentEdits edit = new DocumentEdits(query.getDocument()); - edit.insert(0, "import my.stuff;\n"); - return Optional.of(edit ); + ImportRewrite rewrite = ImportRewrite.create(cu, true); + + javaSnippet.getImports().ifPresent((imprts -> + { + for (String imprt : imprts) { + rewrite.addImport(imprt); + } + })); + + DocumentEdits edit = rewrite.createEdit(query.getDocument()); + + return Optional.of(edit); } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java index 24d16bea2..ed6f7fdc8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.List; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; import org.springframework.ide.vscode.commons.languageserver.util.PrefixFinder; @@ -46,14 +47,14 @@ public class JavaSnippetManager { } - public Collection getCompletions(IDocument doc, int offset, ASTNode node) { + public Collection getCompletions(IDocument doc, int offset, ASTNode node, CompilationUnit cu) { Collection completions = new ArrayList<>(); DocumentRegion query = PREFIX_FINDER.getPrefixRegion(doc, offset); for (JavaSnippet javaSnippet : snippets) { if (FuzzyMatcher.matchScore(query.toString(), javaSnippet.getName()) != 0) { - javaSnippet.generateCompletion(snippetBuilderFactory, query, node) + javaSnippet.generateCompletion(snippetBuilderFactory, query, node, cu) .ifPresent((completion) -> completions.add(completion)); } }