diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java
index 70b6fdd42..fe9370d2a 100644
--- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java
@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine;
@@ -29,6 +30,9 @@ import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider;
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.JavaSnippetManager;
+import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexer;
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider;
@@ -52,6 +56,8 @@ import org.springframework.ide.vscode.commons.maven.MavenCore;
import org.springframework.ide.vscode.commons.maven.MavenProjectFinderStrategy;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
+import com.google.common.collect.ImmutableList;
+
/**
* Language Server for Spring Boot Application Properties files
*
@@ -109,7 +115,66 @@ public class BootJavaLanguageServer extends SimpleLanguageServer {
providers.put(org.springframework.ide.vscode.boot.java.scope.Constants.SPRING_SCOPE, new ScopeCompletionProcessor());
providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, new ValueCompletionProcessor(indexProvider));
- return new BootJavaCompletionEngine(javaProjectFinder, providers);
+ JavaSnippetManager snippetManager = new JavaSnippetManager(this::createSnippetBuilder);
+ snippetManager.add(new JavaSnippet(
+ "RequestMapping method",
+ JavaSnippetContext.BOOT_MEMBERS,
+ CompletionItemKind.Method,
+ ImmutableList.of(
+ "org.springframework.web.bind.annotation.RequestMapping",
+ "org.springframework.web.bind.annotation.RequestMethod",
+ "org.springframework.web.bind.annotation.RequestParam"
+ ),
+ "@RequestMapping(value=\"${path}\", method=RequestMethod.${GET})\n" +
+ "public ${SomeData} ${requestMethodName}(@RequestParam ${String} ${param}) {\n" +
+ " return new ${SomeData}(${cursor});\n" +
+ "}\n"
+ ));
+ snippetManager.add(new JavaSnippet(
+ "GetMapping method",
+ JavaSnippetContext.BOOT_MEMBERS,
+ CompletionItemKind.Method,
+ ImmutableList.of(
+ "org.springframework.web.bind.annotation.GetMapping",
+ "org.springframework.web.bind.annotation.RequestParam"
+ ),
+ "@GetMapping(value=\"${path}\")\n" +
+ "public ${SomeData} ${getMethodName}(@RequestParam ${String} ${param}) {\n" +
+ " return new ${SomeData}(${cursor});\n" +
+ "}\n"
+ ));
+ snippetManager.add(new JavaSnippet(
+ "PostMapping method",
+ JavaSnippetContext.BOOT_MEMBERS,
+ CompletionItemKind.Method,
+ ImmutableList.of(
+ "org.springframework.web.bind.annotation.PostMapping",
+ "org.springframework.web.bind.annotation.RequestBody"
+ ),
+ "@PostMapping(value=\"${path}\")\n" +
+ "public ${SomeEnityData} ${postMethodName}(@RequestBody ${SomeEnityData} ${entity}) {\n" +
+ " //TODO: process POST request\n" +
+ " ${cursor}\n" +
+ " return ${entity};\n" +
+ "}\n"
+ ));
+ snippetManager.add(new JavaSnippet(
+ "PutMapping method",
+ JavaSnippetContext.BOOT_MEMBERS,
+ CompletionItemKind.Method,
+ ImmutableList.of(
+ "org.springframework.web.bind.annotation.PutMapping",
+ "org.springframework.web.bind.annotation.RequestBody",
+ "org.springframework.web.bind.annotation.PathVariable"
+ ),
+ "@PutMapping(value=\"${path}/{${id}}\")\n" +
+ "public ${SomeEnityData} ${putMethodName}(@PathVariable ${pvt:String} ${id}, @RequestBody ${SomeEnityData} ${entity}) {\n" +
+ " //TODO: process PUT request\n" +
+ " ${cursor}\n" +
+ " return ${entity};\n" +
+ "}"
+ ));
+ return new BootJavaCompletionEngine(javaProjectFinder, providers, snippetManager);
}
protected HoverHandler createHoverHandler(JavaProjectFinder javaProjectFinder) {
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 d96daf91d..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
@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.boot.java.handlers;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
@@ -24,6 +25,7 @@ import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.NodeFinder;
+import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetManager;
import org.springframework.ide.vscode.commons.java.IClasspath;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine;
@@ -38,10 +40,12 @@ public class BootJavaCompletionEngine implements ICompletionEngine {
private JavaProjectFinder projectFinder;
private Map completionProviders;
+ private JavaSnippetManager snippets;
- public BootJavaCompletionEngine(JavaProjectFinder projectFinder, Map specificProviders) {
+ public BootJavaCompletionEngine(JavaProjectFinder projectFinder, Map specificProviders, JavaSnippetManager snippets) {
this.projectFinder = projectFinder;
this.completionProviders = specificProviders;
+ this.snippets = snippets;
}
@Override
@@ -69,7 +73,10 @@ public class BootJavaCompletionEngine implements ICompletionEngine {
if (node != null) {
System.out.println("AST node found: " + node.getClass().getName());
- return collectCompletionsForAnnotations(node, offset, document);
+ Collection completions = new ArrayList();
+ completions.addAll(collectCompletionsForAnnotations(node, offset, document));
+ completions.addAll(snippets.getCompletions(document, offset, node, cu));
+ return completions;
}
return Collections.emptyList();
diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java
new file mode 100644
index 000000000..018f68865
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SimpleCompletionFactory.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.handlers;
+
+import org.eclipse.lsp4j.CompletionItemKind;
+import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
+import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
+import org.springframework.ide.vscode.commons.util.Renderable;
+import org.springframework.ide.vscode.commons.util.text.IDocument;
+
+public class SimpleCompletionFactory {
+
+
+ public static class SimpleProposal implements ICompletionProposal{
+
+ private DocumentEdits edits;
+ private CompletionItemKind kind;
+ private Renderable info;
+ private String detail;
+ private String label;
+
+ public SimpleProposal(DocumentEdits edits, CompletionItemKind kind, Renderable info,
+ String detail, String label) {
+ this.edits = edits;
+ this.kind = kind;
+ this.info = info;
+ this.detail = detail;
+ this.label = label;
+ }
+
+ public SimpleProposal setLabel(String label) {
+ this.label = label;
+ return this;
+ }
+
+ @Override
+ public String getLabel() {
+ return label;
+ }
+
+ @Override
+ public DocumentEdits getTextEdit() {
+ return edits;
+ }
+
+ @Override
+ public CompletionItemKind getKind() {
+ return kind;
+ }
+
+ @Override
+ public Renderable getDocumentation() {
+ return info;
+ }
+
+ @Override
+ public String getDetail() {
+ return detail;
+ }
+
+
+ }
+
+ public static SimpleProposal simpleProposal(IDocument doc, int offset, String query, CompletionItemKind kind, String value, String detail, Renderable info) {
+ DocumentEdits edits = new DocumentEdits(doc);
+ edits.replace(offset-query.length(), offset, value);
+ return new SimpleProposal(edits, kind, info, detail, value);
+ }
+}
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
new file mode 100644
index 000000000..0e3fa7755
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippet.java
@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.snippets;
+
+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;
+import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder;
+
+import com.google.common.base.Supplier;
+
+public class JavaSnippet {
+
+ private JavaSnippetContext context;
+
+ private String name;
+
+ private String template;
+
+ private List imports;
+
+ private CompletionItemKind kind;
+
+ public JavaSnippet(String name, JavaSnippetContext context, CompletionItemKind kind, List imports,
+ String template) {
+ super();
+ this.context = context;
+ this.name = name;
+ this.template = template;
+ this.imports = imports;
+ this.kind = kind;
+ }
+
+ public Optional generateCompletion(Supplier snippetBuilderFactory,
+ DocumentRegion query, ASTNode node, CompilationUnit cu) {
+
+ if (context.appliesTo(node)) {
+ return Optional.of(
+ new JavaSnippetCompletion(snippetBuilderFactory,
+ query,
+ cu,
+ this
+ )
+ );
+ }
+
+ return Optional.empty();
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getTemplate() {
+ return this.template;
+ }
+
+ public Optional> getImports() {
+ return Optional.of(this.imports);
+ }
+
+ public CompletionItemKind getKind() {
+ return kind;
+ }
+
+}
diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetBuilder.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetBuilder.java
new file mode 100644
index 000000000..9dfafd966
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetBuilder.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.snippets;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
+import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
+import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder;
+
+import com.google.common.base.Supplier;
+
+/**
+ * Respobsible for converting eclipse-like template string into lsp snippet text.
+ * @author Kris De Volder
+ */
+public class JavaSnippetBuilder{
+
+ private Supplier snippetBuilderFactory;
+
+ private static final Pattern PLACE_HOLDER = Pattern.compile("\\$\\{(.+?)\\}");
+
+ public JavaSnippetBuilder(Supplier snippetBuilderFactory) {
+ this.snippetBuilderFactory = snippetBuilderFactory;
+ }
+
+ public DocumentEdits createEdit(DocumentRegion query, String template) {
+ DocumentEdits edit = new DocumentEdits(query.getDocument());
+ edit.replace(query.getStart(), query.getEnd(), createSnippet(template));
+ return edit;
+ }
+
+ private String createSnippet(String template) {
+ Matcher matcher = PLACE_HOLDER.matcher(template);
+ int start = 0;
+ SnippetBuilder snippet = snippetBuilderFactory.get();
+ while (matcher.find(start)) {
+ int matchStart = matcher.start();
+ snippet.text(template.substring(start, matchStart));
+ int matchEnd = matcher.end();
+ String placeHolderImage = template.substring(matcher.start(1), matcher.end(1));
+ int colon = placeHolderImage.indexOf(':');
+ String id, value;
+ if (colon>=0) {
+ id = placeHolderImage.substring(0, colon);
+ value = placeHolderImage.substring(colon+1);
+ } else {
+ id = placeHolderImage;
+ value = id;
+ }
+ snippet.placeHolder(id, value);
+ start = matchEnd;
+ }
+ snippet.text(template.substring(start));
+ return snippet.build().toString();
+ }
+
+}
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
new file mode 100644
index 000000000..0be2d8366
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetCompletion.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+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;
+import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder;
+import org.springframework.ide.vscode.commons.util.Renderable;
+import org.springframework.ide.vscode.commons.util.Renderables;
+
+import com.google.common.base.Supplier;
+
+public class JavaSnippetCompletion implements ICompletionProposal{
+
+ private DocumentRegion query;
+ private JavaSnippet javaSnippet;
+ private Supplier snippetBuilderFactory;
+ private CompilationUnit cu;
+
+ public JavaSnippetCompletion(Supplier snippetBuilderFactory, DocumentRegion query, CompilationUnit cu, JavaSnippet javaSnippet) {
+ this.snippetBuilderFactory = snippetBuilderFactory;
+ this.query = query;
+ this.cu = cu;
+ this.javaSnippet = javaSnippet;
+ }
+
+ @Override
+ public String getLabel() {
+ return javaSnippet.getName();
+ }
+
+ @Override
+ public CompletionItemKind getKind() {
+ return javaSnippet.getKind();
+ }
+
+ @Override
+ public DocumentEdits getTextEdit() {
+ return new JavaSnippetBuilder(snippetBuilderFactory).createEdit(query, javaSnippet.getTemplate());
+ }
+
+ @Override
+ public String getDetail() {
+ return "Snippet";
+ }
+
+ @Override
+ public Renderable getDocumentation() {
+ return Renderables.NO_DESCRIPTION;
+ }
+
+ @Override
+ public Optional getAdditionalEdit() {
+ 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/JavaSnippetContext.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java
new file mode 100644
index 000000000..f67ffd6e1
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetContext.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.snippets;
+
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.TypeDeclaration;
+
+public interface JavaSnippetContext {
+ JavaSnippetContext BOOT_MEMBERS = (node) -> node instanceof TypeDeclaration;
+
+ boolean appliesTo(ASTNode node);
+}
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
new file mode 100644
index 000000000..ed6f7fdc8
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/JavaSnippetManager.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Pivotal, 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
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Pivotal, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.snippets;
+
+import java.util.ArrayList;
+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;
+import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder;
+import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
+import org.springframework.ide.vscode.commons.util.text.IDocument;
+
+import com.google.common.base.Supplier;
+
+public class JavaSnippetManager {
+
+ private List snippets = new ArrayList<>();
+ private Supplier snippetBuilderFactory;
+
+ private static PrefixFinder PREFIX_FINDER = new PrefixFinder() {
+
+ @Override
+ protected boolean isPrefixChar(char c) {
+ return Character.isJavaIdentifierPart(c);
+ }
+ };
+
+ public JavaSnippetManager(Supplier snippetBuilderFactory) {
+ this.snippetBuilderFactory = snippetBuilderFactory;
+ }
+
+ public void add(JavaSnippet javaSnippet) {
+ snippets.add(javaSnippet);
+
+ }
+
+ 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, cu)
+ .ifPresent((completion) -> completions.add(completion));
+ }
+ }
+
+ return completions;
+ }
+
+}
diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/eclipse-templates.xml b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/eclipse-templates.xml
new file mode 100644
index 000000000..29f2785cc
--- /dev/null
+++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/snippets/eclipse-templates.xml
@@ -0,0 +1,50 @@
+
+
+ ${x:import(org.springframework.web.bind.annotation.RequestMapping,
+ org.springframework.web.bind.annotation.RequestMethod,
+ org.springframework.web.bind.annotation.RequestParam)}@RequestMapping(value="${path}",
+ method=RequestMethod.${GET})
+ public ${SomeData} ${requestMethodName}(@RequestParam ${String} ${param}) {
+ return new ${SomeData}(${cursor});
+ }
+
+
+ ${x:import(org.springframework.web.bind.annotation.GetMapping,
+ org.springframework.web.bind.annotation.RequestParam)}@GetMapping(value="${path}")
+ public ${SomeData} ${getMethodName}(@RequestParam ${String} ${param})
+ {
+ return new ${SomeData}(${cursor});
+ }
+
+
+ ${x:import(org.springframework.web.bind.annotation.PostMapping,
+ org.springframework.web.bind.annotation.RequestBody)}@PostMapping(value="${path}")
+ public ${SomeEnityData} ${postMethodName}(@RequestBody
+ ${SomeEnityData} ${entity}) {
+ //TODO: process POST request
+ ${cursor}
+ return ${entity};
+ }
+
+
+ ${x:import(org.springframework.web.bind.annotation.PutMapping,
+ org.springframework.web.bind.annotation.RequestBody,
+ org.springframework.web.bind.annotation.PathVariable)}@PutMapping(value="${path}/{${id}}")
+ public ${SomeEnityData} ${putMethodName}(@PathVariable
+ ${pvt:link(String,int,long)} ${id}, @RequestBody ${SomeEnityData}
+ ${entity}) {
+ //TODO: process PUT request
+ ${cursor}
+ return ${entity};
+ }
+
+
diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ICompletionProposal.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ICompletionProposal.java
index a48bc91e2..defec6285 100644
--- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ICompletionProposal.java
+++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ICompletionProposal.java
@@ -11,6 +11,8 @@
package org.springframework.ide.vscode.commons.languageserver.completion;
+import java.util.Optional;
+
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.util.Renderable;
@@ -23,6 +25,7 @@ public interface ICompletionProposal {
String getLabel();
CompletionItemKind getKind();
DocumentEdits getTextEdit();
+ default Optional getAdditionalEdit() { return Optional.empty(); }
String getDetail();
Renderable getDocumentation();
diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java
index d730fde76..c8706cb94 100644
--- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java
+++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java
@@ -15,6 +15,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -35,6 +36,8 @@ import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
+import com.google.common.collect.ImmutableList;
+
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@@ -174,7 +177,19 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
private static void resolveItem(TextDocument doc, ICompletionProposal completion, CompletionItem item) throws Exception {
item.setDocumentation(toMarkdown(completion.getDocumentation()));
- adaptEdits(item, doc, completion.getTextEdit());
+ Optional mainEdit = adaptEdits(doc, completion.getTextEdit());
+ if (mainEdit.isPresent()) {
+ item.setTextEdit(mainEdit.get());
+ item.setInsertTextFormat(InsertTextFormat.Snippet);
+ } else {
+ item.setInsertText("");
+ }
+
+ completion.getAdditionalEdit().ifPresent(edit -> {
+ adaptEdits(doc, edit).ifPresent(extraEdit -> {
+ item.setAdditionalTextEdits(ImmutableList.of(extraEdit));
+ });
+ });
}
private static String toMarkdown(Renderable r) {
@@ -184,24 +199,28 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
return null;
}
- private static void adaptEdits(CompletionItem item, TextDocument doc, DocumentEdits edits) throws Exception {
- TextReplace replaceEdit = edits.asReplacement(doc);
- if (replaceEdit==null) {
- //The original edit does nothing.
- item.setInsertText("");
- } else {
- TextDocument newDoc = doc.copy();
- edits.apply(newDoc);
- TextEdit vscodeEdit = new TextEdit();
- vscodeEdit.setRange(doc.toRange(replaceEdit.start, replaceEdit.end-replaceEdit.start));
- if (Boolean.getBoolean("lsp.completions.indentation.enable")) {
- vscodeEdit.setNewText(replaceEdit.newText);
+ private static Optional adaptEdits(TextDocument doc, DocumentEdits edits) {
+ try {
+ TextReplace replaceEdit = edits.asReplacement(doc);
+ if (replaceEdit==null) {
+ //The original edit does nothing.
+ return Optional.empty();
} else {
- vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
+ TextDocument newDoc = doc.copy();
+ edits.apply(newDoc);
+ TextEdit vscodeEdit = new TextEdit();
+ vscodeEdit.setRange(doc.toRange(replaceEdit.start, replaceEdit.end-replaceEdit.start));
+ if (Boolean.getBoolean("lsp.completions.indentation.enable")) {
+ vscodeEdit.setNewText(replaceEdit.newText);
+ } else {
+ vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
+ }
+ //TODO: cursor offset within newText? for now we assume its always at the end.
+ return Optional.of(vscodeEdit);
}
- //TODO: cursor offset within newText? for now we assume its always at the end.
- item.setTextEdit(vscodeEdit);
- item.setInsertTextFormat(InsertTextFormat.Snippet);
+ } catch (Exception e) {
+ Log.log(e);
+ return Optional.empty();
}
}
diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java
index c79aa0615..00546da52 100644
--- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java
+++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PlaceHolderString.java
@@ -10,12 +10,12 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.util;
-import java.util.Map;
-
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.text.IRegion;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
/**
* Represents a string with placeholder inside. Provides methods to retrieve
@@ -68,12 +68,12 @@ public class PlaceHolderString {
}
- private final ImmutableMap