From f95660cfcbdbbc8bb34f550c6c859e62f3667ce8 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 27 Oct 2017 16:58:36 -0700 Subject: [PATCH] Add more test cases for @Component hovers. Handle the case where bean id is set explicitly in the annotation. --- .../boot/java/BootJavaLanguageServer.java | 2 +- .../vscode/boot/java/livehover/ASTUtils.java | 52 +++ .../ActiveProfilesProvider.java | 18 +- .../java/livehover/InjectedIntoProvider.java | 93 ++++-- .../test/InjectedIntoProviderTest.java | 296 +++++++++++++++++- .../languageserver/testharness/Editor.java | 6 + 6 files changed, 419 insertions(+), 48 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java rename headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/{profiles => livehover}/ActiveProfilesProvider.java (90%) diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index b812d3a83..8cf6cf02f 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -34,8 +34,8 @@ import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider; import org.springframework.ide.vscode.boot.java.livehover.InjectedIntoProvider; -import org.springframework.ide.vscode.boot.java.profiles.ActiveProfilesProvider; import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java new file mode 100644 index 000000000..769dd937a --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover; + +import java.util.Optional; + +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.Range; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class ASTUtils { + + public static Optional nameRange(TextDocument doc, Annotation annotation) { + try { + int start = annotation.getTypeName().getStartPosition(); + int len = annotation.getTypeName().getLength(); + if (doc.getSafeChar(start-1)=='@') { + start--; len++; + } + return Optional.of(doc.toRange(start, len)); + } catch (Exception e) { + Log.log(e); + return Optional.empty(); + } + } + + public static Optional getValueAttribute(Annotation annotation) { + if (annotation.isSingleMemberAnnotation()) { + SingleMemberAnnotation sma = (SingleMemberAnnotation) annotation; + Expression valueExp = sma.getValue(); + if (valueExp instanceof StringLiteral) { + StringLiteral stringLit = (StringLiteral) valueExp; + return Optional.ofNullable(stringLit.getLiteralValue()); + } + } + return Optional.empty(); + } + + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java similarity index 90% rename from headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java rename to headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java index 73e3d275d..989813023 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ActiveProfilesProvider.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.profiles; +package org.springframework.ide.vscode.boot.java.livehover; import java.util.Collection; import java.util.List; @@ -33,6 +33,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableSet; +import static org.springframework.ide.vscode.boot.java.livehover.ASTUtils.*; + /** * @author Kris De Volder */ @@ -109,20 +111,6 @@ public class ActiveProfilesProvider implements HoverProvider { return builder.build(); } - private static Optional nameRange(TextDocument doc, Annotation annotation) { - try { - int start = annotation.getTypeName().getStartPosition(); - int len = annotation.getTypeName().getLength(); - if (doc.getSafeChar(start-1)=='@') { - start--; len++; - } - return Optional.of(doc.toRange(start, len)); - } catch (Exception e) { - Log.log(e); - return Optional.empty(); - } - } - private static Optional rangeOf(TextDocument doc, StringLiteral node) { try { int start = node.getStartPosition(); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java index e9bcd1bdc..e52ddeb81 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.livehover; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -19,6 +20,7 @@ import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; @@ -28,6 +30,7 @@ import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; @@ -39,10 +42,13 @@ public class InjectedIntoProvider implements HoverProvider { //Highlight if any running app contains an instance of this component try { if (runningApps.length > 0) { - String beanType = getAnnotatedType(annotation); - if (beanType!=null) { - if (Stream.of(runningApps).anyMatch(app -> !app.getBeans().getBeansOfType(beanType).isEmpty())) { - return ImmutableList.of(doc.toRange(annotation.getStartPosition(), annotation.getLength())); + LiveBean definedBean = getDefinedBean(annotation); + if (definedBean!=null) { + if (Stream.of(runningApps).anyMatch(app -> hasRelevantBeans(app, definedBean))) { + Optional nameRange = ASTUtils.nameRange(doc, annotation); + if (nameRange.isPresent()) { + return ImmutableList.of(nameRange.get()); + } } } } @@ -52,31 +58,63 @@ public class InjectedIntoProvider implements HoverProvider { return ImmutableList.of(); } + private boolean hasRelevantBeans(SpringBootApp app, LiveBean definedBean) { + return findRelevantBeans(app, definedBean).findAny().isPresent(); + } + + private Stream findRelevantBeans(SpringBootApp app, LiveBean definedBean) { + return app.getBeans() + .getBeansOfName(definedBean.getId()) + .stream() + .filter(bean -> + definedBean.getType().equals(bean.getType()) + ); + } + + private LiveBean getDefinedBean(Annotation annotation) { + ITypeBinding beanType = getAnnotatedType(annotation); + if (beanType!=null) { + String id = getBeanId(annotation, beanType); + if (StringUtil.hasText(id)) { + return LiveBean.builder().id(id).type(beanType.getQualifiedName()).build(); + } + } + return null; + } + @Override public CompletableFuture provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, SpringBootApp[] runningApps) { if (runningApps.length>0) { - String beanType = getAnnotatedType(annotation); - if (beanType!=null) { + LiveBean definedBean = getDefinedBean(annotation); + if (definedBean!=null) { StringBuilder hover = new StringBuilder(); - hover.append("**Injection report for `"+beanType+"'**\n\n"); + hover.append("**Injection report for "+showBean(definedBean)+"**\n\n"); boolean hasInterestingApp = false; for (SpringBootApp app : runningApps) { LiveBeansModel beans = app.getBeans(); - boolean isInteresting = !app.getBeans().getBeansOfType(beanType).isEmpty(); - if (isInteresting) { - hasInterestingApp = true; - hover.append(niceAppName(app)+":"); - for (LiveBean bean : beans.getBeansOfType(beanType)) { + List relevantBeans = findRelevantBeans(app, definedBean).collect(Collectors.toList()); + if (!relevantBeans.isEmpty()) { + if (!hasInterestingApp) { + hasInterestingApp = true; + } else { + hover.append("\n\n"); + } + hover.append(niceAppName(app)+":"); + for (LiveBean bean : relevantBeans) { hover.append("\n\n"); - hover.append("Bean with id: `"+bean.getId()+"`\n"); List dependers = beans.getBeansDependingOn(bean.getId()); if (dependers.isEmpty()) { - hover.append("**Not injected anywhere**\n"); + hover.append(showBean(bean)+" exists but is **Not injected anywhere**"); } else { - hover.append("injected into:\n\n"); + hover.append(showBean(definedBean) + " injected into:\n\n"); + boolean firstDependency = true; for (LiveBean dependingBean : dependers) { - hover.append("- "+dependingBean.getType()+"\n"); + if (!firstDependency) { + hover.append("\n"); + } + hover.append("- "+showBean(dependingBean)); + firstDependency = false; } } } @@ -93,14 +131,27 @@ public class InjectedIntoProvider implements HoverProvider { return null; } - private String getAnnotatedType(Annotation annotation) { + private String getBeanId(Annotation annotation, ITypeBinding beanType) { + Optional explicitId = ASTUtils.getValueAttribute(annotation); + if (explicitId.isPresent()) { + return explicitId.get(); + } + String typeName = beanType.getName(); + if (StringUtil.hasText(typeName)) { + return Character.toLowerCase(typeName.charAt(0))+typeName.substring(1); + } + return null; + } + + private String showBean(LiveBean bean) { + return "Bean [id: "+bean.getId()+", type: `"+bean.getType()+"`]"; + } + + private ITypeBinding getAnnotatedType(Annotation annotation) { ASTNode parent = annotation.getParent(); if (parent instanceof TypeDeclaration) { TypeDeclaration typeDecl = (TypeDeclaration) parent; - ITypeBinding binding = typeDecl.resolveBinding(); - if (binding!=null) { - return binding.getQualifiedName(); - } + return typeDecl.resolveBinding(); } return null; } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java index 56bb067ce..4f9db3fed 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java @@ -89,19 +89,16 @@ public class InjectedIntoProviderTest { "}\n" ); editor.assertHighlights("@Component"); - editor.assertHoverExactText("@Component", - "**Injection report for `com.example.FooImplementation'**\n" + + editor.assertTrimmedHover("@Component", + "**Injection report for Bean [id: fooImplementation, type: `com.example.FooImplementation`]**\n" + "\n" + "Process [PID=111, name=`the-app`]:\n" + "\n" + - "Bean with id: `fooImplementation`\n" + - "**Not injected anywhere**\n" + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] exists but is **Not injected anywhere**\n" ); } @Test public void componentWithOneInjection() throws Exception { - //Like componentWithNoInjections but checks whether we are properly using the bean id - // to determine injections. LiveBeansModel beans = LiveBeansModel.builder() .add(LiveBean.builder() .id("fooImplementation") @@ -114,6 +111,65 @@ public class InjectedIntoProviderTest { .dependencies("fooImplementation") .build() ) + .add(LiveBean.builder() + .id("irrelevantBean") + .type("com.example.IrrelevantBean") + .dependencies("myController") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertTrimmedHover("@Component", + "**Injection report for Bean [id: fooImplementation, type: `com.example.FooImplementation`]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `com.example.MyController`]\n" + ); + } + + @Test public void componentWithMultipleInjections() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("otherBean") + .type("com.example.OtherBean") + .dependencies("fooImplementation") + .build() + ) .build(); mockAppProvider.builder() .isSpringBootApp(true) @@ -137,18 +193,236 @@ public class InjectedIntoProviderTest { "}\n" ); editor.assertHighlights("@Component"); - editor.assertHoverExactText("@Component", - "**Injection report for `com.example.FooImplementation'**\n" + + editor.assertTrimmedHover("@Component", + "**Injection report for Bean [id: fooImplementation, type: `com.example.FooImplementation`]**\n" + "\n" + "Process [PID=111, name=`the-app`]:\n" + "\n" + - "Bean with id: `fooImplementation`\n" + - "injected into:\n" + + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] injected into:\n" + "\n" + - "- com.example.MyController\n" + "- Bean [id: myController, type: `com.example.MyController`]\n" + + "- Bean [id: otherBean, type: `com.example.OtherBean`]\n" ); } + @Test public void componentWithMultipleInjectionsAndMultipleProcesses() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("otherBean") + .type("com.example.OtherBean") + .dependencies("fooImplementation") + .build() + ) + .build(); + for (int i = 1; i <= 2; i++) { + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("100"+i) + .processName("app-instance-"+i) + .beans(beans) + .build(); + } + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertTrimmedHover("@Component", + "**Injection report for Bean [id: fooImplementation, type: `com.example.FooImplementation`]**\n" + + "\n" + + "Process [PID=1001, name=`app-instance-1`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `com.example.MyController`]\n" + + "- Bean [id: otherBean, type: `com.example.OtherBean`]\n" + + "\n" + + "Process [PID=1002, name=`app-instance-2`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `com.example.MyController`]\n" + + "- Bean [id: otherBean, type: `com.example.OtherBean`]\n" + ); + } + + @Test public void onlyShowInfoForRelevantBeanId() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("alternateFooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("otherBean") + .type("com.example.OtherBean") + .dependencies("alternateFooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertHoverExactText("@Component", + "**Injection report for Bean [id: fooImplementation, type: `com.example.FooImplementation`]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `com.example.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `com.example.MyController`]" + ); + } + + @Test public void explicitComponentId() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("alternateFooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("otherBean") + .type("com.example.OtherBean") + .dependencies("alternateFooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component(\"alternateFooImplementation\")\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertTrimmedHover("@Component", + "**Injection report for Bean [id: alternateFooImplementation, type: `com.example.FooImplementation`]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: alternateFooImplementation, type: `com.example.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: otherBean, type: `com.example.OtherBean`]\n" + ); + } + + @Test public void hoHoversWhenRunningAppDoesntHaveTheComponent() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("whateverBean") + .type("com.example.UnrelatedComponent") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.UnrelatedComponent") + .dependencies("whateverBean") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("unrelated-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights(/*MONE*/); + editor.assertNoHover("@Component"); + } + @Test public void hoHoversWhenNoRunningApps() throws Exception { Editor editor = harness.newEditor(LanguageId.JAVA, "package com.example;\n" + diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 94a3b67a7..e331fa1f1 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -603,6 +603,12 @@ public class Editor { assertContains(snippet, hoverString(hover)); } + public void assertTrimmedHover(String hoverOver, String expectedHover) throws Exception { + int hoverPosition = getHoverPosition(hoverOver,1); + Hover hover = harness.getHover(doc, doc.toPosition(hoverPosition)); + assertEquals(expectedHover.trim(), hoverString(hover).trim()); + } + public void assertNoHover(String hoverOver) throws Exception { int hoverPosition = getRawText().indexOf(hoverOver) + hoverOver.length() / 2; Hover hover = harness.getHover(doc, doc.toPosition(hoverPosition));