From b866649a3dc4f91fbaae89335ef3f95e92d290db Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Tue, 25 Apr 2017 18:08:05 -0700 Subject: [PATCH] Relaxing yaml proposals for '- '. --- .../ScopeNameCompletionProposal.java | 12 +- .../completions/ValuePropertyKeyProposal.java | 5 - .../boot/common/AbstractPropertyProposal.java | 3 +- .../ReplaceDeprecatedPropertyQuickfix.java | 9 +- .../ApplicationYamlAssistContext.java | 15 +- .../ReplaceDeprecatedYamlQuickfix.java | 9 +- .../completion/DocumentEdits.java | 22 +++ .../completion/ICompletionProposal.java | 10 +- .../completion/ScoreableProposal.java | 18 ++- .../commons/maven/SourceJavadocTest.java | 1 - .../completion/AbstractYamlAssistContext.java | 4 +- .../completion/TransformedCompletion.java | 74 +++++++++ .../yaml/completion/YTypeAssistContext.java | 77 +++++++++- .../yaml/completion/YamlAssistContext.java | 9 ++ .../yaml/completion/YamlCompletionEngine.java | 144 +++++++++--------- .../vscode/concourse/ConcourseEditorTest.java | 127 ++++++++++++++- 16 files changed, 422 insertions(+), 117 deletions(-) create mode 100644 headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java index 3f439ea76..8718dd0c7 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ScopeNameCompletionProposal.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java.completions; 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.languageserver.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -20,7 +21,7 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; * @author Martin Lippert */ public class ScopeNameCompletionProposal implements ICompletionProposal { - + public static final ScopeNameCompletion[] COMPLETIONS = new ScopeNameCompletion[] { new ScopeNameCompletion("\"prototype\"", "prototype", "prototype scope", null, CompletionItemKind.Value), new ScopeNameCompletion("\"singleton\"", "singleton", "singleton scope (default)", null, CompletionItemKind.Value), @@ -29,8 +30,8 @@ public class ScopeNameCompletionProposal implements ICompletionProposal { new ScopeNameCompletion("\"globalSession\"", "globalSession", "globalSession scope", null, CompletionItemKind.Value), new ScopeNameCompletion("\"application\"", "application", "application scope", null, CompletionItemKind.Value), new ScopeNameCompletion("\"websocket\"", "websocket", "websocket scope", null, CompletionItemKind.Value) - }; - + }; + private final IDocument doc; private final int startOffset; private final int endOffset; @@ -45,11 +46,6 @@ public class ScopeNameCompletionProposal implements ICompletionProposal { this.prefix = prefix; } - @Override - public ICompletionProposal deemphasize() { - return null; - } - @Override public String getLabel() { return completion.getLabel(); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ValuePropertyKeyProposal.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ValuePropertyKeyProposal.java index 7ef2721ab..296e40d3f 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ValuePropertyKeyProposal.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/completions/ValuePropertyKeyProposal.java @@ -32,11 +32,6 @@ public class ValuePropertyKeyProposal implements ICompletionProposal { this.documentation = documentation; } - @Override - public ICompletionProposal deemphasize() { - return null; - } - @Override public String getLabel() { return this.label; diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/common/AbstractPropertyProposal.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/common/AbstractPropertyProposal.java index 4786ffdcc..a98ebea26 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/common/AbstractPropertyProposal.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/common/AbstractPropertyProposal.java @@ -77,8 +77,7 @@ public abstract class AbstractPropertyProposal extends ScoreableProposal { } public void deprecate() { if (!isDeprecated()) { - deemphasize(); - deemphasize(); + deemphasize(DEEMP_DEPRECATION); isDeprecated = true; } } diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java index 993e907b2..b76edb6e7 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/quickfix/ReplaceDeprecatedPropertyQuickfix.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.properties.quickfix; 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.languageserver.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Renderable; public class ReplaceDeprecatedPropertyQuickfix implements ICompletionProposal { @@ -29,10 +30,6 @@ public class ReplaceDeprecatedPropertyQuickfix implements ICompletionProposal { // } // }; - @Override - public ICompletionProposal deemphasize() { - throw new UnsupportedOperationException("Not yet implemented"); - } @Override public String getLabel() { throw new UnsupportedOperationException("Not yet implemented"); @@ -53,6 +50,10 @@ public class ReplaceDeprecatedPropertyQuickfix implements ICompletionProposal { public Renderable getDocumentation() { throw new UnsupportedOperationException("Not yet implemented"); } + @Override + public ScoreableProposal deemphasize(double howmuch) { + throw new UnsupportedOperationException("Not yet implemented"); + } // private final QuickfixContext context; // private final SpringPropertyProblem problem; diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java index 1c7948bc3..3a6d4fb48 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/completions/ApplicationYamlAssistContext.java @@ -66,6 +66,8 @@ import org.springframework.ide.vscode.commons.yaml.util.YamlUtil; import com.google.common.collect.ImmutableList; +import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*; + /** * Represents a context insied a "application.yml" file relative to which we can provide * content assistance. @@ -198,7 +200,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon completionFactory.beanProperty(doc.getDocument(), contextPath.toPropString(), getType(), query, p, score, edits, typeUtil) - .deemphasize() //deemphasize because it already exists + .deemphasize(DEEMP_EXISTS) //deemphasize because it already exists ); } } @@ -399,7 +401,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon doc.getDocument(), edits, match, typeUtil ); if (getContextRoot(doc).exists(YamlPath.fromProperty(match.data.getId()))) { - completion.deemphasize(); + completion.deemphasize(DEEMP_EXISTS); } completions.add(completion); } @@ -492,7 +494,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } return null; } - + @Override public Renderable getHoverInfo(YamlPathSegment lastSegment) { // TODO Auto-generated method stub @@ -506,6 +508,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } } + @Override public Renderable getHoverInfo(YamlPathSegment s) { //ApplicationYamlAssistContext implements getHoverInfo directly. so this is not needed. return null; @@ -524,7 +527,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } }; } - + private static Deprecation getDeprecation(TypeUtil typeUtil, Type parentType, String propName) { Map props = typeUtil.getPropertiesMap(parentType, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED); if (props!=null) { @@ -535,7 +538,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } return null; } - + private static Renderable getDescription(TypeUtil typeUtil, Type parentType, String propName) { try { List jes = getAllJavaElements(typeUtil, parentType, propName); @@ -554,7 +557,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon } return Renderables.NO_DESCRIPTION; } - + private static List getAllJavaElements(TypeUtil typeUtil, Type parentType, String propName) { if (propName!=null) { Type beanType = parentType; diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/quickfix/ReplaceDeprecatedYamlQuickfix.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/quickfix/ReplaceDeprecatedYamlQuickfix.java index aaf7d1823..ebb7b2192 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/quickfix/ReplaceDeprecatedYamlQuickfix.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/yaml/quickfix/ReplaceDeprecatedYamlQuickfix.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.yaml.quickfix; 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.languageserver.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Renderable; public class ReplaceDeprecatedYamlQuickfix implements ICompletionProposal { @@ -29,10 +30,6 @@ public class ReplaceDeprecatedYamlQuickfix implements ICompletionProposal { // } // }; - @Override - public ICompletionProposal deemphasize() { - throw new UnsupportedOperationException("Not yet implemented"); - } @Override public String getLabel() { throw new UnsupportedOperationException("Not yet implemented"); @@ -53,6 +50,10 @@ public class ReplaceDeprecatedYamlQuickfix implements ICompletionProposal { public Renderable getDocumentation() { throw new UnsupportedOperationException("Not yet implemented"); } + @Override + public ScoreableProposal deemphasize(double howmuch) { + throw new UnsupportedOperationException("Not yet implemented"); + } // private final QuickfixContext context; // private final SpringPropertyProblem problem; diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 1e7613ac3..002cdca22 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -11,6 +11,9 @@ package org.springframework.ide.vscode.commons.languageserver.completion; import java.util.ArrayList; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.lsp4j.TextEdit; import org.springframework.ide.vscode.commons.util.Assert; @@ -41,6 +44,8 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; */ public class DocumentEdits implements ProposalApplier { + private static final Pattern NON_WS_CHAR = Pattern.compile("\\S"); + // Note: for small number of edits this implementation is okay. // for large number of edits it is potentially slow because of the // way it transforms edit coordinates (a growing chain of @@ -415,4 +420,21 @@ public class DocumentEdits implements ProposalApplier { } } + /** + * Find first non-whitepace insertion edit and transform its contents. + * @param transformFun receives the insertion text of the target edit and the offset of the first non-whitespace character + * as arguments. It should return a replacement text. + */ + public void transformFirstNonWhitespaceEdit(BiFunction transformFun) { + for (Edit edit : edits) { + if (edit instanceof Insertion) { + Insertion insert = (Insertion) edit; + Matcher matcher = NON_WS_CHAR.matcher(insert.text); + if (matcher.find()) { + insert.text = transformFun.apply(matcher.start(), insert.text); + } + } + } + } + } 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 be991cb69..a48bc91e2 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 @@ -19,10 +19,6 @@ import org.springframework.ide.vscode.commons.util.Renderable; */ public interface ICompletionProposal { - /** - * Transforms a proposal to make it standout less somehow. - */ - ICompletionProposal deemphasize(); String getLabel(); CompletionItemKind getKind(); @@ -32,4 +28,10 @@ public interface ICompletionProposal { Renderable getDocumentation(); default String getFilterText() { return getLabel(); } + /** + * Transforms a proposal to make it standout less somehow. + * @param howmuch A 'weight' for the deemphasis. Allowing to deempasize some proposals more than others. + */ + default ICompletionProposal deemphasize(double howmuch) { return this; } + } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java index d7d3ff2c5..3fdb5bc4b 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/ScoreableProposal.java @@ -13,8 +13,17 @@ package org.springframework.ide.vscode.commons.languageserver.completion; import java.util.Comparator; +import org.springframework.ide.vscode.commons.util.Assert; + public abstract class ScoreableProposal implements ICompletionProposal { - private static final double DEEMP_VALUE = 100000; // should be large enough to move deemphasized stuff to bottom of list. + + public static final double DEEMP_EXISTS = 0.1; + public static final double DEEMP_DEPRECATION = 0.2; + public static final double DEEMP_DASH_PROPOSAL = 0.5; + public static final double DEEMP_INDENTED_PROPOSAL = 1.0; + + + private static final double DEEMP_VALUE = 10_000; // should be large enough to move deemphasized stuff to bottom of list. private double deemphasizedBy = 0.0; @@ -27,7 +36,7 @@ public abstract class ScoreableProposal implements ICompletionProposal { if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) { double s1 = ((ScoreableProposal)p1).getScore(); double s2 = ((ScoreableProposal)p2).getScore(); - if (s1==s2) { + if (Math.abs(s1-s2)<1E-5) { String name1 = ((ScoreableProposal)p1).getLabel(); String name2 = ((ScoreableProposal)p2).getLabel(); return name1.compareTo(name2); @@ -43,8 +52,9 @@ public abstract class ScoreableProposal implements ICompletionProposal { return getBaseScore() - deemphasizedBy; } @Override - public ScoreableProposal deemphasize() { - deemphasizedBy+= DEEMP_VALUE; + public ScoreableProposal deemphasize(double howmuch) { + Assert.isLegal(howmuch>0.0); + deemphasizedBy+= howmuch*DEEMP_VALUE; return this; } public boolean isDeemphasized() { diff --git a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/SourceJavadocTest.java b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/SourceJavadocTest.java index b99cc5fbc..59af21b2b 100644 --- a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/SourceJavadocTest.java +++ b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/SourceJavadocTest.java @@ -8,7 +8,6 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ - package org.springframework.ide.vscode.commons.maven; import static org.junit.Assert.assertEquals; diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java index f2f953cb7..a0e05e9b7 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/AbstractYamlAssistContext.java @@ -45,6 +45,7 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { private static PrefixFinder prefixfinder = new PrefixFinder() { + @Override protected boolean isPrefixChar(char c) { return !Character.isWhitespace(c); } @@ -94,7 +95,7 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { } protected SNode getContextNode() throws Exception { - SNode root = (SNode)getContextRoot(getDocument()); + SNode root = getContextRoot(getDocument()); return contextPath.traverse(root); } @@ -119,5 +120,4 @@ public abstract class AbstractYamlAssistContext implements YamlAssistContext { return getHoverInfo(); } - } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java new file mode 100644 index 000000000..601066b4b --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/TransformedCompletion.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * 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.commons.yaml.completion; + +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.languageserver.completion.ScoreableProposal; +import org.springframework.ide.vscode.commons.util.Renderable; + +/** + * Abstract base-class to implement a Completion that is derived from + * another completion with some transformations applied to it. (E.g. add + * extra indentation for relaxed indentation proposals). + * + * @author Kris De Volder + */ +public abstract class TransformedCompletion extends ScoreableProposal { + protected final ICompletionProposal original; + + private DocumentEdits transformedEdit = null; + + public TransformedCompletion(ICompletionProposal proposal) { + this.original = proposal; + } + + protected abstract String tranformLabel(String originalLabel); + protected abstract DocumentEdits transformEdit(DocumentEdits textEdit); + + @Override + public synchronized DocumentEdits getTextEdit() { + if (transformedEdit==null) { + transformedEdit = transformEdit(original.getTextEdit()); + } + return transformedEdit; + } + + + @Override + public String getLabel() { + return tranformLabel(original.getLabel()); + } + + @Override + public CompletionItemKind getKind() { + return original.getKind(); + } + + @Override + public Renderable getDocumentation() { + return original.getDocumentation(); + } + + @Override + public String getDetail() { + return original.getDetail(); + } + + @Override + public double getBaseScore() { + if (original instanceof ScoreableProposal) { + return ((ScoreableProposal) original).getScore(); + } + return 0; + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java index b783c70bc..faa3b632b 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YTypeAssistContext.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.completion; +import static org.assertj.core.api.Assertions.offset; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -45,6 +47,8 @@ import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; import com.google.common.collect.ImmutableList; +import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*; + public class YTypeAssistContext extends AbstractYamlAssistContext { final static Logger logger = LoggerFactory.getLogger(YTypeAssistContext.class); @@ -53,6 +57,17 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { final private YType type; final private YamlAssistContext parent; + /** + * Create a 'relaxed' {@link YTypeAssistContext} that pretends the type expectedin this context + * is something else than what the schema sugests. + */ + public YTypeAssistContext(YTypeAssistContext relaxationTarget, YType relaxedType) { + super(relaxationTarget.getDocument(), relaxationTarget.documentSelector, relaxationTarget.contextPath); + this.parent = relaxationTarget.parent; + this.typeUtil = relaxationTarget.typeUtil; + this.type = relaxedType; + } + public YTypeAssistContext(YTypeAssistContext parent, YamlPath contextPath, YType YType, YTypeUtil typeUtil) { super(parent.getDocument(), parent.documentSelector, contextPath); this.parent = parent; @@ -62,9 +77,9 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { public YTypeAssistContext(TopLevelAssistContext parent, int documentSelector, YType type, YTypeUtil typeUtil) { super(parent.getDocument(), documentSelector, YamlPath.EMPTY); - this.type = type; - this.typeUtil = typeUtil; this.parent = parent; + this.typeUtil = typeUtil; + this.type = type; } @Override @@ -114,7 +129,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { completionFactory().beanProperty(doc.getDocument(), contextPath.toPropString(), getType(), query, p, score, edits, typeUtil) - .deemphasize() //deemphasize because it already exists + .deemphasize(DEEMP_EXISTS) //deemphasize because it already exists ); } } @@ -243,7 +258,6 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { return "TypeContext("+contextPath.toPropString()+"::"+type+")"; } - @Override public Renderable getHoverInfo() { if (parent!=null) { @@ -293,4 +307,59 @@ public class YTypeAssistContext extends AbstractYamlAssistContext { private YTypedProperty getProperty(String name) { return typeUtil.getPropertiesMap(getType()).get(name); } + + @Override + public YamlAssistContext relax() { + try { + if (typeUtil.isSequencable(type)) { + YType itemType = typeUtil.getDomainType(type); + if (itemType!=null) { + return new YTypeAssistContext(this, itemType) { + @Override + public Collection getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { + Collection basicCompletions = super.getCompletions(doc, node, offset); + return addDashes(basicCompletions); + } + }; + } + } + } catch (Exception e) { + Log.log(e); + } + return super.relax(); + } + + private Collection addDashes(Collection basicCompletions) { + if (!basicCompletions.isEmpty()) { + List dashedCompletions = new ArrayList<>(basicCompletions.size()); + for (ICompletionProposal c : basicCompletions) { + dashedCompletions.add( + new TransformedCompletion(c) { + @Override + protected DocumentEdits transformEdit(DocumentEdits textEdit) { + textEdit.transformFirstNonWhitespaceEdit((Integer offset, String insertText) -> { + if (offset > 2) { + String prefix = insertText.substring(offset-2, offset); + if (" ".equals(prefix)) { + //special case don't add the "- " in front, but replace the inserted spaces instead. + return insertText.substring(0, offset-2)+"- "+insertText.substring(offset); + } + } + return insertText.substring(0, offset) + "- "+insertText.substring(offset); + }); + return textEdit; + } + + @Override + protected String tranformLabel(String originalLabel) { + return "- "+originalLabel; + } + }.deemphasize(0.5) + ); + } + return dashedCompletions; + } + return basicCompletions; + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java index 68e306059..184101ea0 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlAssistContext.java @@ -34,4 +34,13 @@ public interface YamlAssistContext extends YamlNavigable { Renderable getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion); YamlDocument getDocument(); + + /** + * Allows a context to implement a 'relaxation' transformation. A relaxed context + * should compute the same proposals as the original context but may also add + * additional proposals. E.g. a {@link YTypeAssistContext} uses this to + * relax contexts for sequence types to include proposals for the elements + * of the sequence. + */ + default YamlAssistContext relax() { return null; } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index 2a6126ba9..89be1757e 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -14,8 +14,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; -import org.eclipse.lsp4j.CompletionItemKind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -23,7 +23,6 @@ import org.springframework.ide.vscode.commons.languageserver.completion.IComplet import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Assert; -import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument; @@ -35,6 +34,8 @@ import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider; import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil; +import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.*; + /** * Implements {@link ICompletionEngine} for .yml file, based on a YamlAssistContextProvider * which has to to be injected into engine via its contructor. @@ -71,15 +72,16 @@ public class YamlCompletionEngine implements ICompletionEngine { SRootNode root = doc.getStructure(); SNode current = root.find(offset); SNode contextNode = getContextNode(doc, current, offset); - List all = new ArrayList<>(getPreciseCompletions(offset, doc, current, contextNode)); - all.addAll(addIndentations(getRelaxedCompletions(offset, doc, contextNode, current))); + List all = new ArrayList<>(getBaseCompletions(offset, doc, current, contextNode, false)); + all.addAll(getMoreIndentedCompletions(offset, doc, contextNode, current)); + all.addAll(getDashedCompletions(offset, doc, contextNode, current)); return all; } return Collections.emptyList(); } - private Collection addIndentations( - Collection completions) { + private Collection addIndentations( + Collection completions) { if (!completions.isEmpty()) { List transformed = new ArrayList<>(); for (ICompletionProposal p : completions) { @@ -91,63 +93,40 @@ public class YamlCompletionEngine implements ICompletionEngine { } public ICompletionProposal indented(ICompletionProposal proposal) { - ScoreableProposal transformed = new ScoreableProposal() { - - DocumentEdits indentedEdit = null; - - @Override - public synchronized DocumentEdits getTextEdit() { - if (indentedEdit==null) { - indentedEdit = proposal.getTextEdit(); - indentedEdit.indentFirstEdit(YamlIndentUtil.INDENT_STR); - } - return indentedEdit; + ScoreableProposal transformed = new TransformedCompletion(proposal) { + @Override public String tranformLabel(String originalLabel) { + return "➔ " + originalLabel; } - - @Override - public String getLabel() { - return "➔ "+proposal.getLabel(); - } - - @Override - public CompletionItemKind getKind() { - return proposal.getKind(); - } - - @Override - public Renderable getDocumentation() { - return proposal.getDocumentation(); - } - - @Override - public String getDetail() { - return proposal.getDetail(); - } - - @Override - public double getBaseScore() { - if (proposal instanceof ScoreableProposal) { - return ((ScoreableProposal) proposal).getBaseScore(); - } - return 0; + @Override public DocumentEdits transformEdit(DocumentEdits originalEdit) { + originalEdit.indentFirstEdit(YamlIndentUtil.INDENT_STR); + return originalEdit; } }; - transformed.deemphasize(); + transformed.deemphasize(DEEMP_INDENTED_PROPOSAL); return transformed; } - private Collection getRelaxedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception { - SNode contextNode = getContextNode(doc, currentNode, offset, YamlIndentUtil.INDENT_BY); - if (preciseContextNode!=contextNode && isRelaxable(contextNode)) { + private Collection getDashedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception { + SNode contextNode = getContextNode(doc, currentNode, offset, "- "); + if (preciseContextNode!=contextNode) { + return getBaseCompletions(offset, doc, currentNode, contextNode, true); + } + return Collections.emptyList(); + } + + + private Collection getMoreIndentedCompletions(int offset, YamlDocument doc, SNode preciseContextNode, SNode currentNode) throws Exception { + SNode contextNode = getContextNode(doc, currentNode, offset, YamlIndentUtil.INDENT_STR); + if (preciseContextNode!=contextNode && isIndentRelaxable(contextNode)) { YamlAssistContext context = getContext(doc, contextNode); if (context!=null) { - return context.getCompletions(doc, currentNode, offset); + return addIndentations(context.getCompletions(doc, currentNode, offset)); } } return Collections.emptyList(); } - private boolean isRelaxable(SNode contextNode) throws Exception { + private boolean isIndentRelaxable(SNode contextNode) throws Exception { return contextNode!=null && ( isBarrenKey(contextNode) || contextNode.getNodeType()==SNodeType.SEQ @@ -163,8 +142,7 @@ public class YamlCompletionEngine implements ICompletionEngine { return false; } - protected Collection getPreciseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode) - throws Exception { + private Collection getBaseCompletions(int offset, YamlDocument doc, SNode current, SNode contextNode, boolean onlyDashes) throws Exception { if (contextNode!=null) { YamlAssistContext context = getContext(doc, contextNode); if (context==null && isDubiousKey(contextNode, offset)) { @@ -173,7 +151,15 @@ public class YamlCompletionEngine implements ICompletionEngine { context = getContext(doc, contextNode); } if (context!=null) { - return context.getCompletions(doc, current, offset); + Collection all = new ArrayList<>(); + if (!onlyDashes) { + all.addAll(context.getCompletions(doc, current, offset)); + } + YamlAssistContext relaxedContext = context.relax(); + if (relaxedContext!=null) { + all.addAll(relaxedContext.getCompletions(doc, current, offset)); + } + return all; } } return Collections.emptyList(); @@ -204,8 +190,7 @@ public class YamlCompletionEngine implements ICompletionEngine { return null; } - protected SNode getContextNode(YamlDocument doc, SNode node, int offset, int adjustIndent) throws Exception { - Assert.isLegal(adjustIndent>=0); //The code doesn't handle negative indents yet. + protected SNode getContextNode(YamlDocument doc, SNode node, int offset, String adjustIndentStr) throws Exception { if (node==null) { return null; } else if (node.getNodeType()==SNodeType.KEY) { @@ -218,22 +203,41 @@ public class YamlCompletionEngine implements ICompletionEngine { return keyNode.getParent(); } } else if (node.getNodeType()==SNodeType.RAW) { - //Treat raw node as a 'key node'. This is basically assuming that is misclasified - // by structure parser because the ':' was not yet typed into the document. + if (adjustIndentStr.startsWith("- ")) { + // We are trying to determine context node for a completion that starts witjh a '- '. + // Yaml indentation rules means we have to treat this differently because '-' doesn't + // have to be indented to be considered as nested under a key node! + int cursorIndent = doc.getColumn(offset); + int nodeIndent = node.getIndent(); + int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); + while (node.getNodeType()!=SNodeType.DOC && ( + nodeIndent==-1 || + nodeIndent>currentIndent || + nodeIndent==currentIndent && node.getNodeType()==SNodeType.SEQ + )) { + node = node.getParent(); + nodeIndent = node.getIndent(); + } + return node; + } else { + //Treat raw node as a 'key node'. This is basically assuming that is misclasified + // by structure parser because the ':' was not yet typed into the document. - //Complication: if line with cursor is empty or the cursor is inside the indentation - // area then the structure may not reflect correctly the context. This is because - // the correct context depends on text the user has not typed yet.(which will change the - // indentation level of the current line. So we must use the cursorIndentation - // rather than the structure-tree to determine the 'context' node. - int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent); - int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent); - int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); - while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) { - node = node.getParent(); - nodeIndent = node.getIndent(); + //Complication: if line with cursor is empty or the cursor is inside the indentation + // area then the structure may not reflect correctly the context. This is because + // the correct context depends on text the user has not typed yet.(which will change the + // indentation level of the current line. So we must use the cursorIndentation + // rather than the structure-tree to determine the 'context' node. + int adjustIndent = adjustIndentStr==null? 0 : adjustIndentStr.length(); + int cursorIndent = YamlIndentUtil.add(doc.getColumn(offset), adjustIndent); + int nodeIndent = YamlIndentUtil.add(node.getIndent(), adjustIndent); + int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent); + while (nodeIndent==-1 || (nodeIndent>=currentIndent && node.getNodeType()!=SNodeType.DOC)) { + node = node.getParent(); + nodeIndent = node.getIndent(); + } + return node; } - return node; } else if (node.getNodeType()==SNodeType.SEQ) { SSeqNode seqNode = (SSeqNode)node; if (seqNode.isInValue(offset)) { @@ -248,7 +252,7 @@ public class YamlCompletionEngine implements ICompletionEngine { } protected SNode getContextNode(YamlDocument doc, SNode node, int offset) throws Exception { - return getContextNode(doc, node, offset, 0); + return getContextNode(doc, node, offset, ""); } protected YamlPath getContextPath(YamlDocument doc, SNode node, int offset) throws Exception { diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index fadb5704e..bfa20dfcf 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -2968,6 +2968,13 @@ public class ConcourseEditorTest { "name", "plan", "public", + //Completions with '-' + "- aggregate", + "- do", + "- get", + "- put", + "- task", + "- try", //Completions for nested context (i.e. task step) "➔ attempts", "➔ config", @@ -2981,9 +2988,9 @@ public class ConcourseEditorTest { "➔ params", "➔ privileged", "➔ tags", - "➔ task", - "➔ timeout" - ); + "➔ timeout", + "➔ task" + ); } @Test public void gotoSymbolInPipeline() throws Exception { @@ -3245,6 +3252,120 @@ public class ConcourseEditorTest { editor.assertProblems("get: ^versi^|resource does not exist"); } + @Test public void relaxedContentAssistContextForListItem_sameLine() throws Exception { + Editor editor; + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " plan: <*>\n" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " plan: \n" + + " - put: <*>\n" + ); + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " plan: pu<*>\n" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " plan: \n" + + " - put: <*>\n" + ); + } + + @Test public void relaxedContentAssistContextForListItem_indented() throws Exception { + Editor editor; + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - get: docker-git\n" + + " trigger: true\n" + + " <*>" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - get: docker-git\n" + + " trigger: true\n" + + " - put: <*>" + ); + } + @Test public void relaxedContentAssistContextForListItem_not_indented() throws Exception { + Editor editor; + + //Note: this is a tricky case because when <*> lines up with 'plan' it will not be considered + // to be a child of 'plan' but a child of the 'job' instead. + // However, for hypothetical '- ' completions we'd have to treat as a child of plan instead! + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - get: docker-git\n" + + " trigger: true\n" + + " <*>" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - get: docker-git\n" + + " trigger: true\n" + + " - put: <*>" + ); + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " <*>" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - put: <*>" + ); + + editor = harness.newEditor( + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " pu<*>" + ); + editor.assertCompletionWithLabel("- put", + "jobs:\n" + + "- name: build-docker-image\n" + + " serial: true\n" + + " plan:\n" + + " - put: <*>" + ); + + + //TODO: cases were the CA query is not a empty string (on same line as key / on next line) +// assertContextualCompletions(conText, "<*>", +// "- put: <*>"); +// +// assertContextualCompletions(conText, "pu<*>", "- put: <*>"); + } + ////////////////////////////////////////////////////////////////////////////// private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {