Hovers for application properties

This commit is contained in:
BoykoAlex
2016-11-22 20:36:53 -05:00
parent 5c6927ac6f
commit 3bf568b6a2
20 changed files with 1142 additions and 514 deletions

View File

@@ -0,0 +1,61 @@
package org.springframework.ide.vscode.commons.jandex;
import java.util.stream.Stream;
import org.jboss.jandex.AnnotationInstance;
import org.springframework.ide.vscode.commons.java.IAnnotation;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IMemberValuePair;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
public class AnnotationImpl implements IAnnotation {
private AnnotationInstance annotation;
private IJavadocProvider javadocProvider;
AnnotationImpl(AnnotationInstance annotation, IJavadocProvider javadocProvider) {
this.annotation = annotation;
this.javadocProvider = javadocProvider;
}
@Override
public String getElementName() {
return annotation.name().toString();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IMemberValuePair> getMemberValuePairs() {
return annotation.values().stream().map(av -> {
return Wrappers.wrap(av);
});
}
@Override
public String toString() {
return annotation.toString();
}
@Override
public int hashCode() {
return annotation.toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AnnotationImpl) {
return annotation.toString().equals(((AnnotationImpl)obj).annotation.toString());
}
return super.equals(obj);
}
}

View File

@@ -0,0 +1,82 @@
package org.springframework.ide.vscode.commons.jandex;
import java.util.stream.Stream;
import org.jboss.jandex.FieldInfo;
import org.springframework.ide.vscode.commons.java.Flags;
import org.springframework.ide.vscode.commons.java.IAnnotation;
import org.springframework.ide.vscode.commons.java.IField;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
class FieldImpl implements IField {
private JandexIndex index;
private FieldInfo field;
private IJavadocProvider javadocProvider;
FieldImpl(JandexIndex index, FieldInfo field, IJavadocProvider javadocProvider) {
this.index = index;
this.field = field;
this.javadocProvider = javadocProvider;
}
@Override
public int getFlags() {
return field.flags();
}
@Override
public IType getDeclaringType() {
return Wrappers.wrap(index, field.declaringClass(), javadocProvider);
}
@Override
public String getElementName() {
return field.name();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
return field.annotations().stream().map(a -> {
return Wrappers.wrap(a, javadocProvider);
});
}
@Override
public boolean isEnumConstant() {
return Flags.isEnum(field.flags());
}
@Override
public String toString() {
return field.toString();
}
@Override
public int hashCode() {
return field.toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FieldImpl) {
return field.toString().equals(((FieldImpl)obj).field.toString());
}
return super.equals(obj);
}
}

View File

@@ -0,0 +1,102 @@
package org.springframework.ide.vscode.commons.jandex;
import java.util.stream.Stream;
import org.jboss.jandex.MethodInfo;
import org.springframework.ide.vscode.commons.java.IAnnotation;
import org.springframework.ide.vscode.commons.java.IJavaType;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
public class MethodImpl implements IMethod {
private static final String JANDEX_CONTRUCTOR_NAME = "<init>";
private JandexIndex index;
private MethodInfo method;
private IJavadocProvider javadocProvider;
MethodImpl(JandexIndex index, MethodInfo method, IJavadocProvider javadocProvider) {
this.index = index;
this.method = method;
this.javadocProvider =javadocProvider;
}
@Override
public int getFlags() {
return method.flags();
}
@Override
public boolean isConstructor() {
return method.name().equals(JANDEX_CONTRUCTOR_NAME);
}
@Override
public IType getDeclaringType() {
return Wrappers.wrap(index, method.declaringClass(), javadocProvider);
}
@Override
public String getElementName() {
return isConstructor() ? getDeclaringType().getElementName() : method.name();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
return method.annotations().stream().map(a -> Wrappers.wrap(a, javadocProvider));
}
@Override
public IJavaType getReturnType() {
return Wrappers.wrap(method.returnType());
}
// @Override
// public String getSignature() {
// StringBuilder sb = new StringBuilder();
// sb.append('(');
// method.parameters().forEach(p -> sb.append(signature(p)));
// sb.append(')');
// sb.append(getReturnType());
// return sb.toString();
// }
@Override
public String toString() {
return method.toString();
}
@Override
public Stream<IJavaType> parameters() {
return method.parameters().stream().map(Wrappers::wrap);
}
@Override
public int hashCode() {
return method.toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MethodImpl) {
return method.toString().equals(((MethodImpl)obj).method.toString());
}
return super.equals(obj);
}
}

View File

@@ -0,0 +1,132 @@
package org.springframework.ide.vscode.commons.jandex;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;
import org.springframework.ide.vscode.commons.java.Flags;
import org.springframework.ide.vscode.commons.java.IAnnotation;
import org.springframework.ide.vscode.commons.java.IField;
import org.springframework.ide.vscode.commons.java.IJavaType;
import org.springframework.ide.vscode.commons.java.IJavadocProvider;
import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
class TypeImpl implements IType {
private ClassInfo info;
private JandexIndex index;
private IJavadocProvider javadocProvider;
TypeImpl(JandexIndex index, ClassInfo info, IJavadocProvider javadocProvider) {
this.info = info;
this.index = index;
this.javadocProvider = javadocProvider;
}
@Override
public int getFlags() {
return info.flags();
}
@Override
public IType getDeclaringType() {
DotName enclosingClass = info.enclosingClass();
return enclosingClass == null ? null : index.getClassByName(enclosingClass);
}
@Override
public String getElementName() {
return info.simpleName() == null ? info.name().local() : info.simpleName();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
// TODO: check correctness!
return info.annotations().get(info.name()).stream().map(a -> Wrappers.wrap(a, javadocProvider));
}
@Override
public boolean isClass() {
return true;
}
@Override
public boolean isEnum() {
return Flags.isEnum(info.flags());
}
@Override
public boolean isInterface() {
return Flags.isInterface(info.flags());
}
@Override
public boolean isAnnotation() {
return Flags.isAnnotation(info.flags());
}
@Override
public String getFullyQualifiedName() {
return info.name().toString();
}
@Override
public IField getField(String name) {
return Wrappers.wrap(index, info.field(name), javadocProvider);
}
@Override
public Stream<IField> getFields() {
return info.fields().stream().map(f -> {
return Wrappers.wrap(index, f, javadocProvider);
});
}
@Override
public IMethod getMethod(String name, Stream<IJavaType> parameters) {
List<Type> typeParameters = parameters.map(Wrappers::from).collect(Collectors.toList());
return Wrappers.wrap(index, info.method(name, typeParameters.toArray(new Type[typeParameters.size()])), javadocProvider);
}
@Override
public Stream<IMethod> getMethods() {
return info.methods().stream().map(m -> {
return Wrappers.wrap(index, m, javadocProvider);
});
}
@Override
public String toString() {
return info.toString();
}
@Override
public int hashCode() {
return info.toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof TypeImpl) {
return info.toString().equals(((TypeImpl)obj).info.toString());
}
return super.equals(obj);
}
}

View File

@@ -2,20 +2,14 @@ package org.springframework.ide.vscode.commons.jandex;
import static org.springframework.ide.vscode.commons.util.Assert.isNotNull;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.Type;
import org.jboss.jandex.Type.Kind;
import org.springframework.ide.vscode.commons.java.Flags;
import org.springframework.ide.vscode.commons.java.IAnnotation;
import org.springframework.ide.vscode.commons.java.IField;
import org.springframework.ide.vscode.commons.java.IJavaType;
@@ -25,257 +19,32 @@ import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IPrimitiveType;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.java.IVoidType;
import org.springframework.ide.vscode.commons.javadoc.IJavadoc;
public class Wrappers {
private static final String JANDEX_CONTRUCTOR_NAME = "<init>";
public static IType wrap(JandexIndex index, ClassInfo info, IJavadocProvider javadocProvider) {
if (info == null) {
return null;
}
return new IType() {
@Override
public int getFlags() {
return info.flags();
}
@Override
public IType getDeclaringType() {
DotName enclosingClass = info.enclosingClass();
return enclosingClass == null ? null : index.getClassByName(enclosingClass);
}
@Override
public String getElementName() {
return info.simpleName() == null ? info.name().local() : info.simpleName();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
// TODO: check correctness!
return info.annotations().get(info.name()).stream().map(a -> wrap(a, javadocProvider));
}
@Override
public boolean isClass() {
return true;
}
@Override
public boolean isEnum() {
return Flags.isEnum(info.flags());
}
@Override
public boolean isInterface() {
return Flags.isInterface(info.flags());
}
@Override
public boolean isAnnotation() {
return Flags.isAnnotation(info.flags());
}
@Override
public String getFullyQualifiedName() {
return info.name().toString();
}
@Override
public IField getField(String name) {
return wrap(index, info.field(name), javadocProvider);
}
@Override
public Stream<IField> getFields() {
return info.fields().stream().map(f -> {
return wrap(index, f, javadocProvider);
});
}
@Override
public IMethod getMethod(String name, Stream<IJavaType> parameters) {
List<Type> typeParameters = parameters.map(Wrappers::from).collect(Collectors.toList());
return wrap(index, info.method(name, typeParameters.toArray(new Type[typeParameters.size()])), javadocProvider);
}
@Override
public Stream<IMethod> getMethods() {
return info.methods().stream().map(m -> {
return wrap(index, m, javadocProvider);
});
}
@Override
public String toString() {
return info.toString();
}
};
return new TypeImpl(index, info, javadocProvider);
}
public static IField wrap(JandexIndex index, FieldInfo field, IJavadocProvider javadocProvider) {
if (field == null) {
return null;
}
return new IField() {
@Override
public int getFlags() {
return field.flags();
}
@Override
public IType getDeclaringType() {
return wrap(index, field.declaringClass(), javadocProvider);
}
@Override
public String getElementName() {
return field.name();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
return field.annotations().stream().map(a -> {
return wrap(a, javadocProvider);
});
}
@Override
public boolean isEnumConstant() {
return Flags.isEnum(field.flags());
}
@Override
public String toString() {
return field.toString();
}
};
return new FieldImpl(index, field, javadocProvider);
}
public static IMethod wrap(JandexIndex index, MethodInfo method, IJavadocProvider javadocProvider) {
isNotNull(index);
isNotNull(method);
return new IMethod() {
@Override
public int getFlags() {
return method.flags();
}
@Override
public boolean isConstructor() {
return method.name().equals(JANDEX_CONTRUCTOR_NAME);
}
@Override
public IType getDeclaringType() {
return wrap(index, method.declaringClass(), javadocProvider);
}
@Override
public String getElementName() {
return isConstructor() ? getDeclaringType().getElementName() : method.name();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IAnnotation> getAnnotations() {
return method.annotations().stream().map(a -> wrap(a, javadocProvider));
}
@Override
public IJavaType getReturnType() {
return wrap(method.returnType());
}
// @Override
// public String getSignature() {
// StringBuilder sb = new StringBuilder();
// sb.append('(');
// method.parameters().forEach(p -> sb.append(signature(p)));
// sb.append(')');
// sb.append(getReturnType());
// return sb.toString();
// }
@Override
public String toString() {
return method.toString();
}
@Override
public Stream<IJavaType> parameters() {
return method.parameters().stream().map(Wrappers::wrap);
}
};
return new MethodImpl(index, method, javadocProvider);
}
public static IAnnotation wrap(AnnotationInstance annotation, IJavadocProvider javadocProvider) {
isNotNull(annotation);
return new IAnnotation() {
@Override
public String getElementName() {
return annotation.name().toString();
}
@Override
public IJavadoc getJavaDoc() {
return javadocProvider == null ? null : javadocProvider.getJavadoc(this);
}
@Override
public boolean exists() {
return true;
}
@Override
public Stream<IMemberValuePair> getMemberValuePairs() {
return annotation.values().stream().map(av -> {
return wrap(av);
});
}
@Override
public String toString() {
return annotation.toString();
}
};
return new AnnotationImpl(annotation, javadocProvider);
}
public static IMemberValuePair wrap(AnnotationValue annotationValue) {
@@ -324,7 +93,7 @@ public class Wrappers {
}
@SuppressWarnings("unchecked")
private static Type from(IJavaType type) {
static Type from(IJavaType type) {
if (type == IPrimitiveType.BOOLEAN) {
return PrimitiveType.BOOLEAN;
} else if (type == IPrimitiveType.BYTE) {

View File

@@ -12,6 +12,7 @@ public interface JavadocConstants {
char[] FIELD_DETAIL= "<!-- ============ FIELD DETAIL =========== -->".toCharArray(); //$NON-NLS-1$
char[] FIELD_SUMMARY = "<!-- =========== FIELD SUMMARY =========== -->".toCharArray(); //$NON-NLS-1$
char[] ENUM_CONSTANT_SUMMARY = "<!-- =========== ENUM CONSTANT SUMMARY =========== -->".toCharArray(); //$NON-NLS-1$
char[] ENUM_CONSTANT_DETAIL = "<!-- ============ ENUM CONSTANT DETAIL =========== -->".toCharArray();
char[] ANNOTATION_TYPE_REQUIRED_MEMBER_SUMMARY = "<!-- =========== ANNOTATION TYPE REQUIRED MEMBER SUMMARY =========== -->".toCharArray(); //$NON-NLS-1$
char[] ANNOTATION_TYPE_OPTIONAL_MEMBER_SUMMARY = "<!-- =========== ANNOTATION TYPE OPTIONAL MEMBER SUMMARY =========== -->".toCharArray(); //$NON-NLS-1$
char[] END_OF_CLASS_DATA = "<!-- ========= END OF CLASS DATA ========= -->".toCharArray(); //$NON-NLS-1$

View File

@@ -17,11 +17,13 @@ public class JavadocContents {
private boolean hasComputedChildrenSections = false;
private int indexOfFieldDetails;
private int indexOfEnumConstantsDetails;
private int indexOfConstructorDetails;
private int indexOfMethodDetails;
private int indexOfEndOfClassData;
private int indexOfFieldsBottom;
private int indexOfEnumConstantsBottom;
private int indexOfAllMethodsTop;
private int indexOfAllMethodsBottom;
@@ -225,10 +227,14 @@ public class JavadocContents {
int lastIndex = CharOperation.indexOf(JavadocConstants.SEPARATOR_START, this.content, false, this.childrenStart);
lastIndex = lastIndex == -1 ? this.childrenStart : lastIndex;
// try to find enum cosntants detail start
this.indexOfEnumConstantsDetails = CharOperation.indexOf(JavadocConstants.ENUM_CONSTANT_DETAIL, this.content, false, lastIndex);
lastIndex = this.indexOfEnumConstantsDetails == -1 ? lastIndex : this.indexOfEnumConstantsDetails;
// try to find field detail start
this.indexOfFieldDetails = CharOperation.indexOf(JavadocConstants.FIELD_DETAIL, this.content, false, lastIndex);
lastIndex = this.indexOfFieldDetails == -1 ? lastIndex : this.indexOfFieldDetails;
// try to find constructor detail start
this.indexOfConstructorDetails = CharOperation.indexOf(JavadocConstants.CONSTRUCTOR_DETAIL, this.content, false, lastIndex);
lastIndex = this.indexOfConstructorDetails == -1 ? lastIndex : this.indexOfConstructorDetails;
@@ -243,6 +249,16 @@ public class JavadocContents {
int[] classDataRange = sanitizeRange(new int[] { indexOfStartOfClassData + JavadocConstants.START_OF_CLASS_DATA.length, indexOfEndOfClassData}, "ul", "li", "div");
this.indexOfEndOfClassData = classDataRange[1];
// try to find enum constants bottom
this.indexOfEnumConstantsBottom = this.indexOfFieldDetails != -1 ? this.indexOfFieldDetails
: this.indexOfConstructorDetails != -1 ? this.indexOfConstructorDetails
: this.indexOfMethodDetails != -1 ? this.indexOfMethodDetails : this.indexOfEndOfClassData;
// Get rid of possible <ul><li> tag wrappers
int[] fieldsRange = sanitizeRange(new int[] {indexOfEnumConstantsDetails + JavadocConstants.ENUM_CONSTANT_DETAIL.length, indexOfEnumConstantsBottom}, "ul", "li", "div");
indexOfEnumConstantsDetails = fieldsRange[0];
indexOfEnumConstantsBottom = fieldsRange[1];
// try to find the field detail end
this.indexOfFieldsBottom =
this.indexOfConstructorDetails != -1 ? this.indexOfConstructorDetails :
@@ -257,7 +273,7 @@ public class JavadocContents {
this.indexOfAllMethodsBottom = this.indexOfEndOfClassData;
// Get rid of possible <ul><li> tag wrappers
int[] fieldsRange = sanitizeRange(new int[] {indexOfFieldDetails + JavadocConstants.FIELD_DETAIL.length, indexOfFieldsBottom}, "ul", "li", "div");
fieldsRange = sanitizeRange(new int[] {indexOfFieldDetails + JavadocConstants.FIELD_DETAIL.length, indexOfFieldsBottom}, "ul", "li", "div");
indexOfFieldDetails = fieldsRange[0];
indexOfFieldsBottom = fieldsRange[1];
@@ -303,8 +319,10 @@ public class JavadocContents {
char[] anchor = String.valueOf(buffer).toCharArray();
int[] range = null;
int top = field.isEnumConstant() ? this.indexOfEnumConstantsDetails : this.indexOfFieldDetails;
int bottom = field.isEnumConstant() ? this.indexOfEnumConstantsBottom : this.indexOfFieldsBottom;
if (this.indexOfFieldDetails == -1 || this.indexOfFieldsBottom == -1) {
if (top == -1 || bottom == -1) {
// the detail section has no top or bottom, so the doc has an unknown format
if (this.unknownFormatAnchorIndexes == null) {
this.unknownFormatAnchorIndexes = new int[(int)type.getFields().count()];
@@ -316,7 +334,7 @@ public class JavadocContents {
this.tempAnchorIndexesCount = this.unknownFormatAnchorIndexesCount;
this.tempLastAnchorFoundIndex = this.unknownFormatLastAnchorFoundIndex;
range = computeChildRange(anchor, this.indexOfFieldsBottom);
range = computeChildRange(anchor, bottom);
this.unknownFormatLastAnchorFoundIndex = this.tempLastAnchorFoundIndex;
this.unknownFormatAnchorIndexesCount = this.tempAnchorIndexesCount;
@@ -325,14 +343,14 @@ public class JavadocContents {
if (this.fieldAnchorIndexes == null) {
this.fieldAnchorIndexes = new int[(int)type.getFields().count()];
this.fieldAnchorIndexesCount = 0;
this.fieldLastAnchorFoundIndex = this.indexOfFieldDetails;
this.fieldLastAnchorFoundIndex = top;
}
this.tempAnchorIndexes = this.fieldAnchorIndexes;
this.tempAnchorIndexesCount = this.fieldAnchorIndexesCount;
this.tempLastAnchorFoundIndex = this.fieldLastAnchorFoundIndex;
range = computeChildRange(anchor, this.indexOfFieldsBottom);
range = computeChildRange(anchor, bottom);
this.fieldLastAnchorFoundIndex = this.tempLastAnchorFoundIndex;
this.fieldAnchorIndexesCount = this.tempAnchorIndexesCount;

View File

@@ -29,14 +29,29 @@ import reactor.util.function.Tuple2;
public class VscodeHoverEngineAdapter implements VscodeHoverEngine {
public enum HoverType {
MARKDOWN,
HTML
}
private HoverInfoProvider hoverInfoProvider;
private SimpleLanguageServer server;
private HoverType type;
final static Logger logger = LoggerFactory.getLogger(VscodeHoverEngineAdapter.class);
public VscodeHoverEngineAdapter(SimpleLanguageServer server, HoverInfoProvider hoverInfoProvider) {
this(server, hoverInfoProvider, HoverType.MARKDOWN);
}
public VscodeHoverEngineAdapter(SimpleLanguageServer server, HoverInfoProvider hoverInfoProvider, HoverType type) {
this.hoverInfoProvider = hoverInfoProvider;
this.server = server;
this.type = type;
}
public void setHoverType(HoverType type) {
this.type = type;
}
@Override
@@ -56,7 +71,7 @@ public class VscodeHoverEngineAdapter implements VscodeHoverEngine {
IRegion region = hoverTuple.getT2();
Range range = doc.toRange(region.getOffset(), region.getLength());
Hover hover = new Hover(Collections.singletonList(hoverInfo.toMarkdown()), range);
Hover hover = new Hover(Collections.singletonList(render(hoverInfo, type)), range);
return Futures.of(hover);
}
@@ -67,4 +82,15 @@ public class VscodeHoverEngineAdapter implements VscodeHoverEngine {
return SimpleTextDocumentService.NO_HOVER;
}
private static String render(Renderable renderable, HoverType type) {
switch (type) {
case HTML:
return renderable.toHtml();
case MARKDOWN:
return renderable.toMarkdown();
default:
return renderable.toMarkdown();
}
}
}

View File

@@ -34,25 +34,34 @@ public class Renderables {
public static final Renderable NO_DESCRIPTION = italic(text(NO_DESCRIPTION_TEXT));
private static Remark getHtmlToMarkdownConverter() {
public static Remark getHtmlToMarkdownConverter() {
return new Remark();
}
public static Renderable htmlBlob(String html) {
@FunctionalInterface
public interface HtmlContentFiller {
void fill(HtmlBuffer buffer);
}
public static Renderable htmlBlob(HtmlContentFiller contentFiller) {
return new Renderable() {
@Override
public void renderAsHtml(HtmlBuffer buffer) {
buffer.raw(html);
contentFiller.fill(buffer);
}
@Override
public void renderAsMarkdown(StringBuilder buffer) {
buffer.append(getHtmlToMarkdownConverter().convert(html));
buffer.append(getHtmlToMarkdownConverter().convert(toHtml()));
}
};
}
public static Renderable htmlBlob(String html) {
return htmlBlob(buffer -> buffer.raw(html));
}
public static Renderable concat(Renderable... pieces) {
return concat(ImmutableList.copyOf(pieces));

View File

@@ -329,8 +329,13 @@ public class Editor {
* computed when hovering mouse at position at the end of first occurrence of
* a given string in the editor.
*/
public void assertHoverText(String afterString, String expectSnippet) {
throw new UnsupportedOperationException("Not implemented yet!");
public void assertHoverText(String afterString, String expectSnippet) throws Exception {
int pos = getRawText().indexOf(afterString);
if (pos>=0) {
pos += afterString.length();
}
Hover hover = harness.getHover(document, document.toPosition(pos));
assertContains(expectSnippet, hover.getContents().toString());
}
public void setSelection(int start, int end) {

View File

@@ -1,2 +1,6 @@
**/classpath.txt
**/bin/**
**/*.log.*
test-projects/**/.classpath
test-projects/**/.project
test-projects/**/.factorypath

View File

@@ -60,7 +60,7 @@ public class ProjectsHarness {
Path testProjectPath = getProjectPath(name);
switch (type) {
case MAVEN:
MavenBuilder.newBuilder(testProjectPath).clean().pack()./*javadoc().*/skipTests().execute();
MavenBuilder.newBuilder(testProjectPath).clean().pack().javadoc().skipTests().execute();
return new MavenJavaProject(testProjectPath.resolve(MavenCore.POM_XML).toFile());
case CLASSPATH_TXT:
MavenBuilder.newBuilder(testProjectPath).clean().pack().skipTests().execute();

View File

@@ -14,10 +14,13 @@ import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.springframework.ide.vscode.application.properties.completions.SpringPropertiesCompletionEngine;
import org.springframework.ide.vscode.application.properties.hover.PropertiesHoverInfoProvider;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.application.properties.reconcile.SpringPropertiesReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter.HoverType;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
@@ -35,6 +38,7 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
private TypeUtilProvider typeUtilProvider;
private VscodeCompletionEngineAdapter completionEngine;
private SpringPropertiesReconcileEngine reconcileEngine;
private VscodeHoverEngineAdapter hoverEngine;
public ApplicationPropertiesLanguageServer(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder javaProjectFinder) {
@@ -57,7 +61,10 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
completionEngine.setMaxCompletionsNumber(40);
documents.onCompletion(completionEngine::getCompletions);
documents.onCompletionResolve(completionEngine::resolveCompletion);
PropertiesHoverInfoProvider hoverInfoProvider = new PropertiesHoverInfoProvider(indexProvider, typeUtilProvider, javaProjectFinder);
hoverEngine = new VscodeHoverEngineAdapter(this, hoverInfoProvider);
documents.onHover(hoverEngine::getHover);
}
public void setMaxCompletionsNumber(int number) {
@@ -68,6 +75,10 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
reconcileEngine.setRecordSyntaxErrors(record);
}
public void setHoverType(HoverType type) {
hoverEngine.setHoverType(type);
}
@Override
protected ServerCapabilities getServerCapabilities() {
ServerCapabilities c = new ServerCapabilities();
@@ -78,6 +89,8 @@ public class ApplicationPropertiesLanguageServer extends SimpleLanguageServer {
completionProvider.setResolveProvider(false);
c.setCompletionProvider(completionProvider);
c.setHoverProvider(true);
return c;
}

View File

@@ -1,12 +1,12 @@
package org.springframework.ide.vscode.application.properties.completions;
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
import static org.springframework.ide.vscode.application.properties.tools.CommonLanguageTools.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.completions.PropertyCompletionFactory;
@@ -30,7 +30,6 @@ import org.springframework.ide.vscode.commons.languageserver.util.BadLocationExc
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.PrefixFinder;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocument;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.Log;
@@ -43,16 +42,8 @@ import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value
import com.google.common.collect.ImmutableList;
class PropertiesCompletionProposalsCalculator {
private static final Pattern SPACES = Pattern.compile(
"(\\s|\\\\\\s)*"
);
private static boolean isValuePrefixChar(char c) {
return !Character.isWhitespace(c) && c!=',';
}
public class PropertiesCompletionProposalsCalculator {
private static final PrefixFinder valuePrefixFinder = new PrefixFinder() {
protected boolean isPrefixChar(char c) {
return isValuePrefixChar(c);
@@ -103,7 +94,7 @@ class PropertiesCompletionProposalsCalculator {
private boolean preferLowerCaseEnums;
private AntlrParser parser;
PropertiesCompletionProposalsCalculator(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, PropertyCompletionFactory completionFactory, IDocument doc, int offset, boolean preferLowerCaseEnums) {
public PropertiesCompletionProposalsCalculator(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, PropertyCompletionFactory completionFactory, IDocument doc, int offset, boolean preferLowerCaseEnums) {
this.index = index;
this.typeUtil = typeUtil;
this.completionFactory = completionFactory;
@@ -260,76 +251,6 @@ class PropertiesCompletionProposalsCalculator {
return postfix;
}
// public static boolean isAssign(char assign) {
// return assign==':'||assign=='=';
// }
//
// private KeyValuePair getAstNodeLine(IDocument doc, int offset) {
// List<KeyValuePair> pairs = parser.parse(doc.get()).ast.getNodes(KeyValuePair.class);
// return findPair(pairs, offset, 0, pairs.size() - 1);
// }
//
// private KeyValuePair findPair(List<KeyValuePair> pairs, int offset, int start, int end) {
// if (start == end) {
// KeyValuePair pair = pairs.get(start);
// if (pair.getOffset() <= offset && offset <= pair.getOffset() + pair.getLength()) {
// return pair;
// } else {
// return null;
// }
// } else if (start < end ) {
// int pivotIndex = (start + end) / 2;
// KeyValuePair pair = pairs.get(pivotIndex);
// if (pair.getOffset() > offset) {
// return findPair(pairs, offset, start, pivotIndex - 1);
// } else if (offset > pair.getOffset() + pair.getLength()) {
// return findPair(pairs, offset, pivotIndex + 1, end);
// } else {
// return pair;
// }
// } else {
// return null;
// }
// }
// private HoverInfo getValueHoverInfo(DocumentRegion value) {
// try {
// String valueString = value.toString();
// IDocument doc = value.getDocument();
// ITypedRegion valuePartition = getPartition(value.getDocument(), value.getStart());
// int valuePartitionStart = valuePartition.getOffset();
// String propertyName = fuzzySearchPrefix.getPrefix(doc, valuePartitionStart); //note: no need to skip whitespace backwards.
// //because value partition includes whitespace around the assignment
//
// Type type = getValueType(propertyName);
// if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
// //It is useful to provide content assist for the values in the list when entering a list
// type = TypeUtil.getDomainType(type);
// }
// if (TypeUtil.isClass(type)) {
// //Special case. We want to provide hoverinfos more liberally than what's suggested for completions (i.e. even class names
// //that are not suggested by the hints because they do not meet subtyping constraints should be hoverable and linkable!
// StsValueHint hint = StsValueHint.className(valueString, typeUtil);
// if (hint!=null) {
// return new ValueHintHoverInfo(hint);
// }
// }
// //Hack: pretend to invoke content-assist at the end of the value text. This should provide hints applicable to that value
// // then show hoverinfo based on that. That way we can avoid duplication a lot of similar logic to compute hoverinfos and hyperlinks.
// Collection<StsValueHint> hints = getValueHints(valueString, propertyName, EnumCaseMode.ALIASED);
// if (hints!=null) {
// for (StsValueHint h : hints) {
// if (valueString.equals(h.getValue())) {
// return new ValueHintHoverInfo(h);
// }
// }
// }
// } catch (BadLocationException e) {
// Log.log(e);
// }
// return null;
// }
private Collection<ICompletionProposal> getValueCompletions(Value value) {
DocumentRegion valueRegion = createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES);
String query = valuePrefixFinder.getPrefix(doc, offset, valueRegion.getStart());
@@ -340,7 +261,7 @@ class PropertiesCompletionProposalsCalculator {
String propertyName = /*fuzzySearchPrefix.getPrefix(doc, pair.getOffset())*/value.getParent().getKey().decode();
// because value partition includes whitespace around the assignment
if (propertyName != null) {
Collection<StsValueHint> valueCompletions = getValueHints(query, propertyName, caseMode);
Collection<StsValueHint> valueCompletions = getValueHints(index, typeUtil, query, propertyName, caseMode);
if (valueCompletions != null && !valueCompletions.isEmpty()) {
ArrayList<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
for (StsValueHint hint : valueCompletions) {
@@ -350,7 +271,7 @@ class PropertiesCompletionProposalsCalculator {
DocumentEdits edits = new DocumentEdits(doc);
edits.delete(startOfValue, offset);
edits.insert(offset, valueCandidate);
proposals.add(completionFactory.valueProposal(valueCandidate, query, getValueType(propertyName),
proposals.add(completionFactory.valueProposal(valueCandidate, query, getValueType(index, typeUtil, propertyName),
score, edits, new ValueHintHoverInfo(hint))
// new ValueProposal(startOfValue, valuePrefix,
// valueCandidate, i)
@@ -374,54 +295,6 @@ class PropertiesCompletionProposalsCalculator {
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length);
}
private Collection<StsValueHint> getValueHints(String query, String propertyName, EnumCaseMode caseMode) {
Type type = getValueType(propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
List<StsValueHint> allHints = new ArrayList<>();
{
Collection<StsValueHint> hints = typeUtil.getHintValues(type, query, caseMode);
if (CollectionUtil.hasElements(hints)) {
allHints.addAll(hints);
}
}
{
PropertyInfo prop = index.findLongestCommonPrefixEntry(propertyName);
if (prop!=null) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
allHints.addAll(hintProvider.getValueHints(query));
}
}
}
return allHints;
}
/**
* Determine the value type for a give propertyName.
*/
protected Type getValueType(String propertyName) {
try {
PropertyInfo prop = index.get(propertyName);
if (prop!=null) {
return TypeParser.parse(prop.getType());
} else {
prop = findLongestValidProperty(index, propertyName);
if (prop!=null) {
TextDocument doc = new TextDocument(null);
doc.setText(propertyName);
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, new DocumentRegion(doc, 0, doc.getLength()));
return navigator.navigate(prop.getId().length(), TypeParser.parse(prop.getType()));
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
private List<Match<PropertyInfo>> findMatches(String prefix) {
List<Match<PropertyInfo>> matches = index.find(camelCaseToHyphens(prefix));
return matches;
@@ -462,121 +335,4 @@ class PropertiesCompletionProposalsCalculator {
return Collections.emptyList();
}
// public HoverInfo getHoverInfo(IDocument doc, IRegion _region) {
// debug("getHoverInfo("+_region+")");
//
// //The delegate 'getHoverRegion' for spring propery editor will return smaller word regions.
// // we must ensure to use our own region finder to identify correct property name.
// ITypedRegion region = getHoverRegion(doc, _region.getOffset());
// if (region!=null) {
// String contentType = region.getType();
// try {
// if (contentType.equals(IDocument.DEFAULT_CONTENT_TYPE)) {
// debug("hoverRegion = "+region);
// PropertyInfo best = findBestHoverMatch(doc.get(region.getOffset(), region.getLength()).trim());
// if (best!=null) {
// return new SpringPropertyHoverInfo(documentContextFinder.getJavaProject(doc), best);
// }
// } else if (contentType.equals(IPropertiesFilePartitions.PROPERTY_VALUE)) {
// return getValueHoverInfo(new DocumentRegion(doc, region));
// }
// } catch (Exception e) {
// SpringPropertiesEditorPlugin.log(e);
// }
// }
// return null;
// }
//
// public ITypedRegion getHoverRegion(IDocument document, int offset) {
// try {
// ITypedRegion candidate = getPartition(document, offset);
// if (candidate!=null) {
// String type = candidate.getType();
// if (IDocument.DEFAULT_CONTENT_TYPE.equals(type)) {
// return candidate;
// } else if (IPropertiesFilePartitions.PROPERTY_VALUE.equals(type)) {
// DocumentRegion valueRegion = new DocumentRegion(document, candidate).trimStart(ASSIGN);
// return getValueHoverRegion(valueRegion, valueRegion.toRelative(offset));
// }
// }
// } catch (Exception e) {
// SpringPropertiesEditorPlugin.log(e);
// }
// return null;
// }
//
// private ITypedRegion getValueHoverRegion(DocumentRegion r, int offset) {
// int len = r.length();
// if (offset>=0 && offset<=len) {
// int start = offset;
// while (start>0 && isValuePrefixChar(r.charAt(start-1))) {
// start--;
// }
// int end = offset;
// while (end<len && isValuePrefixChar(r.charAt(end))) {
// end++;
// }
// r = r.subSequence(start, end);
// if (!r.isEmpty()) {
// return r.asTypedRegion(IPropertiesFilePartitions.PROPERTY_VALUE);
// }
// }
// return null;
// }
// /**
// * Search known properties for the best 'match' to show as hover data.
// */
// private PropertyInfo findBestHoverMatch(String propName) {
// //TODO: optimize, should be able to use index's treemap to find this without iterating all entries.
// PropertyInfo best = null;
// int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix
// int bestExtraLen = Integer.MAX_VALUE;
// for (PropertyInfo candidate : index) {
// int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId());
// int extraLen = candidate.getId().length()-commonPrefixLen;
// if (commonPrefixLen==propName.length() && extraLen==0) {
// //exact match found, can stop searching for better matches
// return candidate;
// }
// //candidate is better if...
// if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix
// || commonPrefixLen==bestCommonPrefixLen && extraLen<bestExtraLen //or same common prefix but fewer extra chars
// ) {
// bestCommonPrefixLen = commonPrefixLen;
// bestExtraLen = extraLen;
// best = candidate;
// }
// }
// return best;
// }
/**
* Find the longest known property that is a prefix of the given name. Here prefix does not mean
* 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So
* 'prefix' is not allowed to end in the middle of a 'segment'.
*/
public static PropertyInfo findLongestValidProperty(FuzzyMap<PropertyInfo> index, String name) {
int bracketPos = name.indexOf('[');
int endPos = bracketPos>=0?bracketPos:name.length();
PropertyInfo prop = null;
String prefix = null;
while (endPos>0 && prop==null) {
prefix = name.substring(0, endPos);
String canonicalPrefix = camelCaseToHyphens(prefix);
prop = index.get(canonicalPrefix);
if (prop==null) {
endPos = name.lastIndexOf('.', endPos-1);
}
}
if (prop!=null) {
//We should meet caller's expectation that matched properties returned by this method
// match the names exactly even if we found them using relaxed name matching.
return prop.withId(prefix);
}
return null;
}
}

View File

@@ -0,0 +1,127 @@
package org.springframework.ide.vscode.application.properties.hover;
import java.util.Collection;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.util.HtmlBuffer;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.StringUtil;
public abstract class AbstractPropertyRenderableProvider {
/**
* Fake host name that is used in 'action link' urls so that we can
* recognize them as such.
*/
private static final String ACTION_HOST = "action";
Renderable getRenderable() {
return new Renderable() {
@Override
public void renderAsHtml(HtmlBuffer buffer) {
// JavaTypeLinks jtLinks = new JavaTypeLinks(this);
renderId(buffer);
String type = getType();
if (type==null) {
type = Object.class.getName();
}
// jtLinks.javaTypeLink(html, getJavaProject(), type);
actionLink(buffer, type);
String deflt = formatDefaultValue(getDefaultValue());
if (deflt!=null) {
buffer.raw("<br><br>");
buffer.text("Default: ");
buffer.raw("<i>");
buffer.text(deflt);
buffer.raw("</i>");
}
if (isDeprecated()) {
buffer.raw("<br><br>");
String reason = getDeprecationReason();
if (StringUtil.hasText(reason)) {
buffer.bold("Deprecated: ");
buffer.text(reason);
} else {
buffer.bold("Deprecated!");
}
}
Renderable description = getDescription();
if (description!=null) {
buffer.raw("<br><br>");
buffer.p(description.toHtml());
}
}
@Override
public void renderAsMarkdown(StringBuilder buffer) {
buffer.append(Renderables.getHtmlToMarkdownConverter().convert(toHtml()));
}
};
}
final protected void renderId(HtmlBuffer html) {
boolean deprecated = isDeprecated();
String tag = deprecated ? "s" : "b";
String replacement = getDeprecationReplacement();
html.raw("<"+tag+">");
html.text(getId());
html.raw("</"+tag+">");
if (StringUtil.hasText(replacement)) {
html.text(" -> "+ replacement);
}
html.raw("<br>");
}
protected abstract Object getDefaultValue();
protected abstract IJavaProject getJavaProject();
protected abstract Renderable getDescription();
protected abstract String getType();
protected abstract String getDeprecationReason();
protected abstract String getId();
protected abstract String getDeprecationReplacement();
protected abstract boolean isDeprecated();
public static String formatDefaultValue(Object defaultValue) {
if (defaultValue!=null) {
if (defaultValue instanceof String) {
return (String) defaultValue;
} else if (defaultValue instanceof Number) {
return ((Number)defaultValue).toString();
} else if (defaultValue instanceof Boolean) {
return Boolean.toString((Boolean) defaultValue);
} else if (defaultValue instanceof Object[]) {
return StringUtil.arrayToCommaDelimitedString((Object[]) defaultValue);
} else if (defaultValue instanceof Collection<?>) {
return StringUtil.collectionToCommaDelimitedString((Collection<?>) defaultValue);
} else {
//no idea what it is but try 'toString' and hope for the best
return defaultValue.toString();
}
}
return null;
}
/**
* Creates an 'action' link and adds it to the html buffer. When the user clicks the given
* link then the provided runnable is to be executed.
*/
public void actionLink(HtmlBuffer html, String displayString/*, Runnable runnable*/) {
// String actionId = registerAction(runnable);
html.raw("<a href=\"http://"+ACTION_HOST+"/");
html.url("action-id");
html.raw("\">");
html.text(displayString);
html.raw("</a>");
}
}

View File

@@ -0,0 +1,164 @@
package org.springframework.ide.vscode.application.properties.hover;
import static org.springframework.ide.vscode.application.properties.tools.CommonLanguageTools.SPACES;
import static org.springframework.ide.vscode.application.properties.tools.CommonLanguageTools.getValueHints;
import static org.springframework.ide.vscode.application.properties.tools.CommonLanguageTools.getValueType;
import java.util.Collection;
import java.util.Optional;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.util.BadLocationException;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.IRegion;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
class PropertiesHoverCalculator {
private FuzzyMap<PropertyInfo> index;
private TypeUtil typeUtil;
private IJavaProject project;
private IDocument doc;
private int offset;
private AntlrParser parser;
PropertiesHoverCalculator(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, IJavaProject project, IDocument doc, int offset) {
this.index = index;
this.typeUtil = typeUtil;
this.project = project;
this.doc = doc;
this.offset = offset;
this.parser = new AntlrParser();
}
Tuple2<Renderable, IRegion> calculate() {
ParseResults parseResults = parser.parse(doc.get());
Node node = parseResults.ast.findNode(offset);
if (node instanceof Value) {
return getValueHover((Value)node);
} else if (node instanceof Key) {
return getPropertyHover((Key)node);
}
return null;
}
private DocumentRegion createRegion(IDocument doc, Node value) {
// Trim trailing spaces (there is no leading white space already)
int length = value.getLength();
try {
length = doc.get(value.getOffset(), value.getLength()).length();
} catch (BadLocationException e) {
// ignore
}
return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length);
}
private Tuple2<Renderable, IRegion> getPropertyHover(Key property) {
PropertyInfo best = findBestHoverMatch(property.decode());
if (best == null) {
return null;
} else {
Renderable renderable = new PropertyRenderableProvider(project, best).getRenderable();
DocumentRegion region = createRegion(doc, property);
return Tuples.of(renderable, region.asRegion());
}
}
private Tuple2<Renderable, IRegion> getValueHover(Value value) {
DocumentRegion valueRegion = createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES);
if (valueRegion.getStart() <= offset && offset < valueRegion.getEnd()) {
String valueString = valueRegion.toString();
String propertyName = value.getParent().getKey().decode();
Type type = getValueType(index, typeUtil, propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
if (TypeUtil.isClass(type)) {
//Special case. We want to provide hoverinfos more liberally than what's suggested for completions (i.e. even class names
//that are not suggested by the hints because they do not meet subtyping constraints should be hoverable and linkable!
StsValueHint hint = StsValueHint.className(valueString, typeUtil);
if (hint!=null) {
return Tuples.of(createRenderable(hint), valueRegion.asRegion());
}
}
//Hack: pretend to invoke content-assist at the end of the value text. This should provide hints applicable to that value
// then show hoverinfo based on that. That way we can avoid duplication a lot of similar logic to compute hoverinfos and hyperlinks.
Collection<StsValueHint> hints = getValueHints(index, typeUtil, valueString, propertyName, EnumCaseMode.ALIASED);
if (hints!=null) {
Optional<StsValueHint> hint = hints.stream().filter(h -> valueString.equals(h.getValue())).findFirst();
if (hint.isPresent()) {
return Tuples.of(createRenderable(hint.get()), valueRegion.asRegion());
}
}
}
return null;
}
private Renderable createRenderable(StsValueHint hint) {
return Renderables.htmlBlob((html) -> {
/*
* HACK: javadoc comment from HTML javadoc provider coming from
* generated HTML javadoc is very rich and decorating it further
* with some header like labels just makes it look worse
*/
String descriptionHtml = hint.getDescription().toHtml();
if (descriptionHtml.indexOf("<h4>") == -1) {
// Simple text like description without proper header
html.bold(hint.getValue());
html.raw("<p>");
html.raw(hint.getDescription().toHtml());
html.raw("</p>");
} else {
// Description is javadoc-like description from HTML javadoc
html.raw(hint.getDescription().toHtml());
}
});
}
/**
* Search known properties for the best 'match' to show as hover data.
*/
private PropertyInfo findBestHoverMatch(String propName) {
//TODO: optimize, should be able to use index's treemap to find this without iterating all entries.
PropertyInfo best = null;
int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix
int bestExtraLen = Integer.MAX_VALUE;
for (PropertyInfo candidate : index) {
int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId());
int extraLen = candidate.getId().length()-commonPrefixLen;
if (commonPrefixLen==propName.length() && extraLen==0) {
//exact match found, can stop searching for better matches
return candidate;
}
//candidate is better if...
if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix
|| commonPrefixLen==bestCommonPrefixLen && extraLen<bestExtraLen //or same common prefix but fewer extra chars
) {
bestCommonPrefixLen = commonPrefixLen;
bestExtraLen = extraLen;
best = candidate;
}
}
return best;
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.ide.vscode.application.properties.hover;
import org.springframework.ide.vscode.application.properties.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtilProvider;
import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.util.IDocument;
import org.springframework.ide.vscode.commons.languageserver.util.IRegion;
import org.springframework.ide.vscode.commons.util.Renderable;
import reactor.util.function.Tuple2;
public class PropertiesHoverInfoProvider implements HoverInfoProvider {
private SpringPropertyIndexProvider indexProvider;
private TypeUtilProvider typeUtilProvider;
private JavaProjectFinder projectFinder;
public PropertiesHoverInfoProvider(SpringPropertyIndexProvider indexProvider, TypeUtilProvider typeUtilProvider, JavaProjectFinder projectFinder) {
this.indexProvider = indexProvider;
this.typeUtilProvider = typeUtilProvider;
this.projectFinder = projectFinder;
}
@Override
public Tuple2<Renderable, IRegion> getHoverInfo(IDocument document, int offset) throws Exception {
return new PropertiesHoverCalculator(indexProvider.getIndex(document),
typeUtilProvider.getTypeUtil(document), projectFinder.find(document), document, offset).calculate();
}
}

View File

@@ -0,0 +1,218 @@
package org.springframework.ide.vscode.application.properties.hover;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo.PropertySource;
import org.springframework.ide.vscode.commons.java.IJavaElement;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.IMethod;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.StringUtil;
/**
* Information object that is displayed in SpringPropertiesTextHover's information
* control.
* <p>
* Essentially this is a wrapper around {@link ConfigurationMetadataProperty}
*
* @author Kris De Volder
*/
public class PropertyRenderableProvider extends AbstractPropertyRenderableProvider {
/**
* Java project which is used to find declaration for 'navigate to declaration' action
*/
private IJavaProject javaProject;
/**
* Data object to display in 'hover text'
*/
private PropertyInfo data;
public PropertyRenderableProvider(IJavaProject project, PropertyInfo data) {
this.javaProject = project;
this.data = data;
}
public PropertyInfo getElement() {
return data;
}
public boolean canOpenDeclaration() {
return getJavaElements()!=null;
}
/**
* Like 'getSources' but converts raw info into IJavaElements. Raw data which fails to be converted
* is silenetly ignored.
*/
public List<IJavaElement> getJavaElements() {
try {
if (javaProject!=null) {
List<PropertySource> sources = getSources();
if (!sources.isEmpty()) {
ArrayList<IJavaElement> elements = new ArrayList<IJavaElement>();
for (PropertySource source : sources) {
String typeName = source.getSourceType();
if (typeName!=null) {
IType type = javaProject.findType(typeName);
IMethod method = null;
if (type!=null) {
String methodSig = source.getSourceMethod();
if (methodSig!=null) {
method = getMethod(type, methodSig);
} else {
method = getSetter(type, getElement());
}
}
if (method!=null) {
elements.add(method);
} else if (type!=null) {
elements.add(type);
}
}
}
return elements;
}
} else {
}
} catch (Exception e) {
Log.log(e);
}
return Collections.emptyList();
}
/**
* Attempt to find corresponding setter method for a given property.
* @return setter method, or null if not found.
*/
private IMethod getSetter(IType type, PropertyInfo propertyInfo) {
try {
String propName = propertyInfo.getName();
String setterName = "set"
+Character.toUpperCase(propName.charAt(0))
+toCamelCase(propName.substring(1));
String sloppySetterName = setterName.toLowerCase();
IMethod sloppyMatch = null;
for (IMethod m : type.getMethods().collect(Collectors.toList())) {
String mname = m.getElementName();
if (setterName.equals(mname)) {
//found 'exact' name match... done
return m;
} else if (mname.toLowerCase().equals(sloppySetterName)) {
sloppyMatch = m;
}
}
return sloppyMatch;
} catch (Exception e) {
Log.log(e);
return null;
}
}
/**
* Convert hyphened name to camel case name. It is
* safe to call this on an already camel-cased name.
*/
private String toCamelCase(String name) {
if (name.isEmpty()) {
return name;
} else {
StringBuilder camel = new StringBuilder();
char[] chars = name.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (c=='-') {
i++;
if (i<chars.length) {
camel.append(Character.toUpperCase(chars[i]));
}
} else {
camel.append(chars[i]);
}
}
return camel.toString();
}
}
/**
* Get 'raw' info about sources that define this property.
*/
public List<PropertySource> getSources() {
return data.getSources();
}
private IMethod getMethod(IType type, String methodSig) {
int nameEnd = methodSig.indexOf('(');
String name;
if (nameEnd>=0) {
name = methodSig.substring(0, nameEnd);
} else {
name = methodSig;
}
//TODO: This code assumes 0 arguments, which is the case currently for all
// 'real' data in spring jars.
IMethod m = type.getMethod(name, Stream.empty());
if (m!=null) {
return m;
}
//try find a method with the same name.
return type.getMethods().filter(meth -> name.equals(meth.getElementName())).findFirst().orElse(null);
}
@Override
protected Object getDefaultValue() {
return data.getDefaultValue();
}
@Override
protected IJavaProject getJavaProject() {
return javaProject;
}
@Override
protected Renderable getDescription() {
String desc = data.getDescription();
if (StringUtil.hasText(desc)) {
return Renderables.text(desc);
}
return null;
}
@Override
protected String getType() {
return data.getType();
}
@Override
protected String getDeprecationReason() {
return data.getDeprecationReason();
}
@Override
protected String getId() {
return data.getId();
}
@Override
protected String getDeprecationReplacement() {
return data.getDeprecationReplacement();
}
@Override
protected boolean isDeprecated() {
return data.isDeprecated();
}
}

View File

@@ -0,0 +1,109 @@
package org.springframework.ide.vscode.application.properties.tools;
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.application.properties.metadata.PropertyInfo;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProvider;
import org.springframework.ide.vscode.application.properties.metadata.hints.HintProviders;
import org.springframework.ide.vscode.application.properties.metadata.hints.StsValueHint;
import org.springframework.ide.vscode.application.properties.metadata.types.Type;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeParser;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil;
import org.springframework.ide.vscode.application.properties.metadata.types.TypeUtil.EnumCaseMode;
import org.springframework.ide.vscode.application.properties.metadata.util.FuzzyMap;
import org.springframework.ide.vscode.application.properties.reconcile.PropertyNavigator;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocument;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.Log;
public class CommonLanguageTools {
public static final Pattern SPACES = Pattern.compile(
"(\\s|\\\\\\s)*"
);
public static boolean isValuePrefixChar(char c) {
return !Character.isWhitespace(c) && c!=',';
}
/**
* Determine the value type for a give propertyName.
*/
public static Type getValueType(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, String propertyName) {
try {
PropertyInfo prop = index.get(propertyName);
if (prop!=null) {
return TypeParser.parse(prop.getType());
} else {
prop = CommonLanguageTools.findLongestValidProperty(index, propertyName);
if (prop!=null) {
TextDocument doc = new TextDocument(null);
doc.setText(propertyName);
PropertyNavigator navigator = new PropertyNavigator(doc, null, typeUtil, new DocumentRegion(doc, 0, doc.getLength()));
return navigator.navigate(prop.getId().length(), TypeParser.parse(prop.getType()));
}
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
public static Collection<StsValueHint> getValueHints(FuzzyMap<PropertyInfo> index, TypeUtil typeUtil, String query, String propertyName, EnumCaseMode caseMode) {
Type type = getValueType(index, typeUtil, propertyName);
if (TypeUtil.isArray(type) || TypeUtil.isList(type)) {
//It is useful to provide content assist for the values in the list when entering a list
type = TypeUtil.getDomainType(type);
}
List<StsValueHint> allHints = new ArrayList<>();
{
Collection<StsValueHint> hints = typeUtil.getHintValues(type, query, caseMode);
if (CollectionUtil.hasElements(hints)) {
allHints.addAll(hints);
}
}
{
PropertyInfo prop = index.findLongestCommonPrefixEntry(propertyName);
if (prop!=null) {
HintProvider hintProvider = prop.getHints(typeUtil, false);
if (!HintProviders.isNull(hintProvider)) {
allHints.addAll(hintProvider.getValueHints(query));
}
}
}
return allHints;
}
/**
* Find the longest known property that is a prefix of the given name. Here prefix does not mean
* 'string prefix' but a prefix in the sense of treating '.' as a kind of separators. So
* 'prefix' is not allowed to end in the middle of a 'segment'.
*/
public static PropertyInfo findLongestValidProperty(FuzzyMap<PropertyInfo> index, String name) {
int bracketPos = name.indexOf('[');
int endPos = bracketPos>=0?bracketPos:name.length();
PropertyInfo prop = null;
String prefix = null;
while (endPos>0 && prop==null) {
prefix = name.substring(0, endPos);
String canonicalPrefix = camelCaseToHyphens(prefix);
prop = index.get(canonicalPrefix);
if (prop==null) {
endPos = name.lastIndexOf('.', endPos-1);
}
}
if (prop!=null) {
//We should meet caller's expectation that matched properties returned by this method
// match the names exactly even if we found them using relaxed name matching.
return prop.withId(prefix);
}
return null;
}
}

View File

@@ -20,6 +20,8 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Diagnostic;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.ide.vscode.application.properties.ApplicationPropertiesLanguageServer;
@@ -27,7 +29,7 @@ import org.springframework.ide.vscode.application.properties.metadata.CachingVal
import org.springframework.ide.vscode.application.properties.metadata.PropertiesLoader;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.IType;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter.HoverType;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
@@ -37,9 +39,6 @@ import org.springframework.ide.vscode.properties.editor.test.harness.StyledStrin
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Diagnostic;
/**
* Boot App Properties Editor tests
*
@@ -48,8 +47,6 @@ import org.eclipse.lsp4j.Diagnostic;
*/
public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
private JavaProjectFinder javaProjectFinder;
@Test public void testReconcileCatchesParseError() throws Exception {
Editor editor = newEditor("key\n");
editor.assertProblems("key|extraneous input");
@@ -171,7 +168,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
}
@Ignore @Test public void testHoverInfos() throws Exception {
@Test public void testHoverInfos() throws Exception {
defaultTestData();
Editor editor = newEditor(
"#foo\n" +
@@ -187,7 +184,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
editor.assertHoverText("logging.", "<b>logging.level</b>");
}
@Ignore @Test public void testHoverInfosWithSpaces() throws Exception {
@Test public void testHoverInfosWithSpaces() throws Exception {
defaultTestData();
Editor editor = newEditor(
"#foo\n" +
@@ -204,7 +201,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
editor.assertHoverText("logging.", "<b>logging.level</b>");
}
@Ignore @Test public void testHoverLongAndShort() throws Exception {
@Test public void testHoverLongAndShort() throws Exception {
data("server.port", INTEGER, 8080, "Port where server listens for http.");
data("server.port.fancy", BOOLEAN, 8080, "Whether the port is fancy.");
Editor editor = newEditor(
@@ -917,7 +914,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
);
}
@Ignore @Test public void testDeprecatedPropertyHoverInfo() throws Exception {
@Test public void testDeprecatedPropertyHoverInfo() throws Exception {
data("error.path", "java.lang.String", null, "Path of the error controller.");
Editor editor = newEditor(
"# a comment\n"+
@@ -1334,7 +1331,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
}
}
@Ignore @Test public void testClassReferenceCompletion() throws Exception {
@Ignore @Test public void testClassReferenceCompletion() throws Exception {
CachingValueProvider.TIMEOUT = Duration.ofSeconds(20);
useProject(createPredefinedMavenProject("empty-boot-1.3.0-with-mongo"));
@@ -1523,7 +1520,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
);
}
@Ignore @Test public void testEnumJavaDocShownInValueHover() throws Exception {
@Test public void testEnumJavaDocShownInValueHover() throws Exception {
useProject(createPredefinedMavenProject("enums-boot-1.3.2-app"));
data("my.background", "demo.Color", null, "Color to use as default background.");
@@ -1563,6 +1560,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest {
protected SimpleLanguageServer newLanguageServer() {
ApplicationPropertiesLanguageServer server = new ApplicationPropertiesLanguageServer(md.getIndexProvider(), typeUtilProvider, javaProjectFinder);
server.setMaxCompletionsNumber(-1);
server.setHoverType(HoverType.HTML);
return server;
}