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 @@ + + + + + + + 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 placeHolders; + private final ImmutableMultimap placeHolders; private final String string; - public PlaceHolderString(Map placeHolders, String string) { + public PlaceHolderString(Multimap placeHolders, String string) { super(); - this.placeHolders = ImmutableMap.copyOf(placeHolders); + this.placeHolders = ImmutableMultimap.copyOf(placeHolders); this.string = string; } @@ -90,7 +90,11 @@ public class PlaceHolderString { } public PlaceHolder getPlaceHolder(Object id) { - return placeHolders.get(id); + ImmutableCollection all = placeHolders.get(id); + if (all!=null && !all.isEmpty()) { + return all.iterator().next(); + } + return null; } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PrefixFinder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PrefixFinder.java index d96acc7e2..9674f0041 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PrefixFinder.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/PrefixFinder.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Pivotal, Inc. + * Copyright (c) 2015, 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 @@ -32,4 +32,9 @@ public abstract class PrefixFinder { return getPrefix(doc, offset, 0); } protected abstract boolean isPrefixChar(char c); + + public DocumentRegion getPrefixRegion(IDocument doc, int offset) { + String prefix = getPrefix(doc, offset); + return prefix != null ? new DocumentRegion(doc, offset - prefix.length(), offset) : null; + } } \ No newline at end of file diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java index 5d5dc1aa7..5796ec14c 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SnippetBuilder.java @@ -11,10 +11,16 @@ package org.springframework.ide.vscode.commons.languageserver.util; import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.springframework.ide.vscode.commons.languageserver.util.PlaceHolderString.PlaceHolder; +import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.text.Region; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; + public class SnippetBuilder { /** @@ -25,7 +31,10 @@ public class SnippetBuilder { public static SnippetBuilder gimped() { return new SnippetBuilder() { @Override - protected String createPlaceHolder(int id) { + protected String createPlaceHolder(Object id, Optional value) { + if (value.isPresent()) { + return value.get(); + } return ""; } }; @@ -34,7 +43,9 @@ public class SnippetBuilder { private static final int FIRST_PLACE_HOLDER_ID = 1; private int nextPlaceHolderId = FIRST_PLACE_HOLDER_ID; private StringBuilder buf = new StringBuilder(); - private HashMap placeHolders = new HashMap<>(); + private Multimap placeHolders = MultimapBuilder.hashKeys().arrayListValues().build(); + + private Map idMap = new HashMap<>(); public SnippetBuilder text(String text) { buf.append(text); @@ -47,7 +58,29 @@ public class SnippetBuilder { public SnippetBuilder placeHolder() { int offset = buf.length(); int id = nextPlaceHolderId++; - buf.append(createPlaceHolder(id)); + buf.append(createPlaceHolder(id, Optional.empty())); + int end = buf.length(); + placeHolders.put(id, new PlaceHolderString.PlaceHolder(id, new Region(offset, end-offset))); + return this; + } + + public SnippetBuilder placeHolder(String name, String _value) { + Assert.isNotNull(_value); + int offset = buf.length(); + Object id; + Optional value; + if (name.equals("cursor")) { + id = 0; + value = Optional.empty(); + } else { + id = idMap.get(name); + if (id==null) { + id = nextPlaceHolderId++; + idMap.put(name, id); + } + value = Optional.of(_value); + } + buf.append(createPlaceHolder(id, value)); int end = buf.length(); placeHolders.put(id, new PlaceHolderString.PlaceHolder(id, new Region(offset, end-offset))); return this; @@ -62,8 +95,12 @@ public class SnippetBuilder { * The default implementation creates place holder strings that * match format specified by LSP 3.0. */ - protected String createPlaceHolder(int id) { - return "$"+id; + protected String createPlaceHolder(Object id, Optional value) { + if (!value.isPresent()) { + return "$"+id; + } else { + return "${"+id+":"+value.get()+"}"; + } } public PlaceHolderString build() { @@ -74,7 +111,7 @@ public class SnippetBuilder { public String toString() { String str = buf.toString(); if (getPlaceholderCount()==1 ) { - String placeHolder = createPlaceHolder(FIRST_PLACE_HOLDER_ID); + String placeHolder = createPlaceHolder(FIRST_PLACE_HOLDER_ID, Optional.empty()); if (str.endsWith(placeHolder)) { str = str.substring(0, str.length()-placeHolder.length()); } @@ -99,7 +136,7 @@ public class SnippetBuilder { * @return The number of placeholder that where inserted in the snippet. */ public int getPlaceholderCount() { - return nextPlaceHolderId-1; + return placeHolders.size(); } } diff --git a/vscode-extensions/commons-vscode/package.json b/vscode-extensions/commons-vscode/package.json index 6c8b24e1c..dbab4b1f0 100644 --- a/vscode-extensions/commons-vscode/package.json +++ b/vscode-extensions/commons-vscode/package.json @@ -30,6 +30,6 @@ "typescript": "^2.3.0", "@types/node": "^6.0.68", "vscode": "^1.1.5", - "vscode-languageclient": "^3.2.0" + "vscode-languageclient": "^3.4.2" } } diff --git a/vscode-extensions/vscode-boot-java/package.json b/vscode-extensions/vscode-boot-java/package.json index 93d703067..919ec396d 100644 --- a/vscode-extensions/vscode-boot-java/package.json +++ b/vscode-extensions/vscode-boot-java/package.json @@ -35,7 +35,7 @@ "vsce-package": "vsce package" }, "dependencies": { - "vscode-languageclient": "^3.2.0", + "vscode-languageclient": "^3.4.2", "commons-vscode": "^0.0.6" }, "devDependencies": { diff --git a/vscode-extensions/vscode-boot-properties/package.json b/vscode-extensions/vscode-boot-properties/package.json index 85999a5b4..74668adf2 100644 --- a/vscode-extensions/vscode-boot-properties/package.json +++ b/vscode-extensions/vscode-boot-properties/package.json @@ -40,7 +40,7 @@ "vsce-package": "vsce package" }, "dependencies": { - "vscode-languageclient": "^3.2.0", + "vscode-languageclient": "^3.4.2", "commons-vscode": "^0.0.6" }, "devDependencies": { diff --git a/vscode-extensions/vscode-bosh/package.json b/vscode-extensions/vscode-bosh/package.json index f77727e6c..10b81acfb 100644 --- a/vscode-extensions/vscode-bosh/package.json +++ b/vscode-extensions/vscode-bosh/package.json @@ -98,7 +98,7 @@ "vsce-package": "vsce package" }, "dependencies": { - "vscode-languageclient": "^3.2.x", + "vscode-languageclient": "^3.4.2", "commons-vscode": "^0.0.6" }, "devDependencies": { diff --git a/vscode-extensions/vscode-concourse/package.json b/vscode-extensions/vscode-concourse/package.json index 4cd75a0fd..0b9778d8c 100644 --- a/vscode-extensions/vscode-concourse/package.json +++ b/vscode-extensions/vscode-concourse/package.json @@ -82,7 +82,7 @@ "vsce-package": "vsce package" }, "dependencies": { - "vscode-languageclient": "^3.2.x", + "vscode-languageclient": "^3.4.2", "commons-vscode": "^0.0.6" }, "devDependencies": { diff --git a/vscode-extensions/vscode-manifest-yaml/package.json b/vscode-extensions/vscode-manifest-yaml/package.json index 5af27f8c2..7d1df7160 100644 --- a/vscode-extensions/vscode-manifest-yaml/package.json +++ b/vscode-extensions/vscode-manifest-yaml/package.json @@ -57,7 +57,7 @@ "vsce-package": "vsce package" }, "dependencies": { - "vscode-languageclient": "^3.2.0", + "vscode-languageclient": "^3.4.2", "commons-vscode": "^0.0.6" }, "devDependencies": {