diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java index 106df05ec0..9004aa45dd 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandler.java @@ -27,6 +27,7 @@ import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.ISourceReference; import org.eclipse.jdt.core.IType; @@ -103,11 +104,11 @@ public CodeLens resolve(CodeLens lens, IProgressMonitor monitor) { JavaLanguageServerPlugin.logException(e.getMessage(), e); } } else if (IMPLEMENTATION_TYPE.equals(type)) { - if (element instanceof IType typeElement) { + if (element instanceof IType || element instanceof IMethod) { try { IDocument document = JsonRpcHelpers.toDocument(typeRoot.getBuffer()); int offset = document.getLineOffset(position.getLine()) + position.getCharacter(); - locations = findImplementations(typeRoot, typeElement, offset, monitor); + locations = findImplementations(typeRoot, element, offset, monitor); } catch (CoreException | BadLocationException e) { JavaLanguageServerPlugin.logException(e.getMessage(), e); } @@ -128,12 +129,15 @@ public CodeLens resolve(CodeLens lens, IProgressMonitor monitor) { return lens; } - private List findImplementations(ITypeRoot root, IType type, int offset, IProgressMonitor monitor) throws CoreException { + private List findImplementations(ITypeRoot root, IJavaElement element, int offset, IProgressMonitor monitor) throws CoreException { //java.lang.Object is a special case. We need to minimize heavy cost of I/O, // by avoiding opening all files from the Object hierarchy - boolean useDefaultLocation = "java.lang.Object".equals(type.getFullyQualifiedName()); + boolean useDefaultLocation = false; + if (element instanceof IType type) { + useDefaultLocation = "java.lang.Object".equals(type.getFullyQualifiedName()); + } ImplementationToLocationMapper mapper = new ImplementationToLocationMapper(preferenceManager.isClientSupportsClassFileContent(), useDefaultLocation); - ImplementationCollector searcher = new ImplementationCollector<>(root, new Region(offset, 0), type, mapper); + ImplementationCollector searcher = new ImplementationCollector<>(root, new Region(offset, 0), element, mapper); return searcher.findImplementations(monitor); } @@ -228,7 +232,8 @@ private void collectCodeLenses(ITypeRoot typeRoot, IJavaElement[] elements, Coll } } } - if (preferenceManager.getPreferences().isImplementationsCodeLensEnabled() && element instanceof IType type) { + String implementationsPreference = preferenceManager.getPreferences().getImplementationsCodeLens(); + if (("all".equals(implementationsPreference) || "types".equals(implementationsPreference)) && element instanceof IType type) { if (type.isInterface() || Flags.isAbstract(type.getFlags())) { CodeLens lens = getCodeLens(IMPLEMENTATION_TYPE, element, typeRoot); if (lens != null) { @@ -236,6 +241,15 @@ private void collectCodeLenses(ITypeRoot typeRoot, IJavaElement[] elements, Coll } } } + + if (("all".equals(implementationsPreference) || "methods".equals(implementationsPreference)) && element instanceof IMethod methodElement) { + if ((methodElement.getParent() instanceof IType parentType && parentType.isInterface()) || Flags.isAbstract(methodElement.getFlags())) { + CodeLens lens = getCodeLens(IMPLEMENTATION_TYPE, element, typeRoot); + if (lens != null) { + lenses.add(lens); + } + } + } } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java index 935642c540..775a513cee 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java @@ -230,9 +230,9 @@ public class Preferences { public static final String REFERENCES_CODE_LENS_ENABLED_KEY = "java.referencesCodeLens.enabled"; /** - * Preference key to enable/disable implementation code lenses. + * Preference key to set implementation code lenses. */ - public static final String IMPLEMENTATIONS_CODE_LENS_ENABLED_KEY = "java.implementationsCodeLens.enabled"; + public static final String IMPLEMENTATIONS_CODE_LENS_KEY = "java.implementationCodeLens"; /** * Preference key to enable/disable formatter. @@ -628,7 +628,7 @@ public class Preferences { private boolean mavenDownloadSources; private boolean eclipseDownloadSources; private boolean mavenUpdateSnapshots; - private boolean implementationsCodeLensEnabled; + private String implementationsCodeLens; private boolean javaFormatEnabled; private String javaQuickFixShowAt; private boolean javaFormatOnTypeEnabled; @@ -906,7 +906,7 @@ public Preferences() { eclipseDownloadSources = false; mavenUpdateSnapshots = false; referencesCodeLensEnabled = true; - implementationsCodeLensEnabled = false; + implementationsCodeLens = "none"; javaFormatEnabled = true; javaQuickFixShowAt = LINE; javaFormatOnTypeEnabled = false; @@ -1071,8 +1071,8 @@ public static Preferences createFrom(Map configuration) { prefs.setMavenUpdateSnapshots(updateSnapshots); boolean referenceCodelensEnabled = getBoolean(configuration, REFERENCES_CODE_LENS_ENABLED_KEY, true); prefs.setReferencesCodelensEnabled(referenceCodelensEnabled); - boolean implementationCodeLensEnabled = getBoolean(configuration, IMPLEMENTATIONS_CODE_LENS_ENABLED_KEY, false); - prefs.setImplementationCodelensEnabled(implementationCodeLensEnabled); + String implementationCodeLens = getString(configuration, IMPLEMENTATIONS_CODE_LENS_KEY, "none"); + prefs.setImplementationCodelens(implementationCodeLens); boolean javaFormatEnabled = getBoolean(configuration, JAVA_FORMAT_ENABLED_KEY, true); prefs.setJavaFormatEnabled(javaFormatEnabled); @@ -1578,8 +1578,8 @@ public void setSignatureHelpDescriptionEnabled(boolean signatureHelpDescriptionE this.signatureHelpDescriptionEnabled = signatureHelpDescriptionEnabled; } - private Preferences setImplementationCodelensEnabled(boolean enabled) { - this.implementationsCodeLensEnabled = enabled; + private Preferences setImplementationCodelens(String implementationCodeLensOption) { + this.implementationsCodeLens = implementationCodeLensOption; return this; } @@ -1908,12 +1908,12 @@ public boolean isMavenUpdateSnapshots() { return mavenUpdateSnapshots; } - public boolean isImplementationsCodeLensEnabled() { - return implementationsCodeLensEnabled; + public String getImplementationsCodeLens() { + return implementationsCodeLens; } public boolean isCodeLensEnabled() { - return referencesCodeLensEnabled || implementationsCodeLensEnabled; + return referencesCodeLensEnabled || !implementationsCodeLens.equals("none"); } public boolean isJavaFormatEnabled() { diff --git a/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Ext.java b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Ext.java new file mode 100644 index 0000000000..7a92e8a5a3 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Ext.java @@ -0,0 +1,10 @@ +package java; + +public class Ext extends Test{ + + @Override + public void testMethod() {} + + @Override + public void testAbstractMethod() {} +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/ITest.java b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/ITest.java new file mode 100644 index 0000000000..2c3fca36d5 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/ITest.java @@ -0,0 +1,7 @@ +package java; + +public interface ITest { + + public void testMethod(); + +} diff --git a/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Test.java b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Test.java new file mode 100644 index 0000000000..4f72eab39b --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/java/Test.java @@ -0,0 +1,11 @@ +package java; + +public abstract class Test implements ITest { + + @Override + public void testMethod() {} + + public abstract void testAbstractMethod(); + + public void noReferences() {} +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandlerTest.java index 5091fcc5c7..b1a3e10a7b 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CodeLensHandlerTest.java @@ -163,7 +163,7 @@ public void testGetCodeLensSymbols() throws Exception { @Test @SuppressWarnings("unchecked") public void testGetCodeLensSymbolsForClass() throws Exception { - Preferences implementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_ENABLED_KEY, "true")); + Preferences implementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_KEY, "types")); Mockito.reset(preferenceManager); when(preferenceManager.getPreferences()).thenReturn(implementationsCodeLenses); handler = new CodeLensHandler(preferenceManager); @@ -214,7 +214,7 @@ public void testDisableCodeLensSymbols() throws Exception { @Test public void testEnableImplementationsCodeLensSymbols() throws Exception { - Preferences implementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_ENABLED_KEY, "true")); + Preferences implementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_KEY, "types")); Mockito.reset(preferenceManager); when(preferenceManager.getPreferences()).thenReturn(implementationsCodeLenses); handler = new CodeLensHandler(preferenceManager); @@ -238,7 +238,7 @@ public void testEnableImplementationsCodeLensSymbols() throws Exception { @Test public void testDisableImplementationsCodeLensSymbols() throws Exception { - Preferences noImplementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_ENABLED_KEY, "false")); + Preferences noImplementationsCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.IMPLEMENTATIONS_CODE_LENS_KEY, "types")); Mockito.reset(preferenceManager); when(preferenceManager.getPreferences()).thenReturn(noImplementationsCodeLenses); Preferences noReferencesCodeLenses = Preferences.createFrom(Collections.singletonMap(Preferences.REFERENCES_CODE_LENS_ENABLED_KEY, "false")); @@ -298,6 +298,86 @@ public void testResolveImplementationsCodeLens() { assertRange(5, 13, 17, loc.getRange()); } + @SuppressWarnings("unchecked") + @Test + public void testResolveImplementationsInterfaceMethodCodeLens() { + String source = "src/java/ITest.java"; + String payload = createCodeLensImplementationsRequest(source, 4, 16, 28); + + CodeLens lens = getParams(payload); + Range range = lens.getRange(); + assertRange(4, 16, 28, range); + + CodeLens result = handler.resolve(lens, monitor); + assertNotNull(result); + + // Check if command found + Command command = result.getCommand(); + assertNotNull(command); + assertEquals("2 implementations", command.getTitle()); + assertEquals("java.show.implementations", command.getCommand()); + + // Check codelens args + List args = command.getArguments(); + assertEquals(3, args.size()); + + // Check we point to the Bar class + String sourceUri = args.get(0).toString(); + assertTrue(sourceUri.endsWith("ITest.java")); + + // CodeLens position + Position p = (Position) args.get(1); + assertEquals(4, p.getLine()); + assertEquals(16, p.getCharacter()); + + // Reference location (just checking implementation in Test.java) + List locations = (List) args.get(2); + assertEquals(2, locations.size()); + Location loc = locations.stream().filter(l -> l.getUri().contains("Test")).findFirst().get(); + assertTrue(loc.getUri().endsWith("src/java/Test.java")); + assertRange(5, 13, 23, loc.getRange()); + } + + @SuppressWarnings("unchecked") + @Test + public void testResolveImplementationsAbstractMethodCodeLens() { + String source = "src/java/Test.java"; + String payload = createCodeLensImplementationsRequest(source, 7, 25, 45); + + CodeLens lens = getParams(payload); + Range range = lens.getRange(); + assertRange(7, 25, 45, range); + + CodeLens result = handler.resolve(lens, monitor); + assertNotNull(result); + + // Check if command found + Command command = result.getCommand(); + assertNotNull(command); + assertEquals("1 implementation", command.getTitle()); + assertEquals("java.show.implementations", command.getCommand()); + + // Check codelens args + List args = command.getArguments(); + assertEquals(3, args.size()); + + // Check we point to the Bar class + String sourceUri = args.get(0).toString(); + assertTrue(sourceUri.endsWith("Test.java")); + + // CodeLens position + Position p = (Position) args.get(1); + assertEquals(7, p.getLine()); + assertEquals(25, p.getCharacter()); + + // Reference location (just checking implementation in Test.java) + List locations = (List) args.get(2); + assertEquals(1, locations.size()); + Location loc = locations.stream().filter(l -> l.getUri().contains("Ext")).findFirst().get(); + assertTrue(loc.getUri().endsWith("src/java/Ext.java")); + assertRange(8, 13, 31, loc.getRange()); + } + @SuppressWarnings("unchecked") @Test public void testResolveCodeLense() {