diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java index 0d39fe47a5..382f2a6fd5 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java @@ -221,6 +221,7 @@ public void registerCapabilities(InitializeResult initializeResult) { semanticTokensOptions.setDocumentSelector(List.of(new DocumentFilter("java", "file", null), new DocumentFilter("java", "jdt", null))); semanticTokensOptions.setLegend(SemanticTokensHandler.legend()); capabilities.setSemanticTokensProvider(semanticTokensOptions); + capabilities.setTypeHierarchyProvider(Boolean.TRUE); initializeResult.setCapabilities(capabilities); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java index 0c17a9fd6e..504f7202c9 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java @@ -146,6 +146,10 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.TypeDefinitionParams; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; import org.eclipse.lsp4j.WillSaveTextDocumentParams; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceSymbol; @@ -179,6 +183,7 @@ public class JDTLanguageServer extends BaseJDTLanguageServer implements Language private ClasspathUpdateHandler classpathUpdateHandler; private JVMConfigurator jvmConfigurator; private WorkspaceExecuteCommandHandler commandHandler; + private TypeHierarchyHandler typeHierarchyHandler = new TypeHierarchyHandler(); private ProgressReporterManager progressReporterManager; /** @@ -1158,6 +1163,24 @@ public CompletableFuture checkExtractInterfaceSta return computeAsync((monitor) -> ExtractInterfaceHandler.checkExtractInterfaceStatus(params)); } + @Override + public CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params) { + logInfo(">> textDocument/prepareTypeHierarchy"); + return computeAsync(monitor -> typeHierarchyHandler.prepareTypeHierarchy(params, monitor)); + } + + @Override + public CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params) { + logInfo(">> typeHierarchy/supertypes"); + return computeAsync(monitor -> typeHierarchyHandler.getSupertypeItems(params, monitor)); + } + + @Override + public CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params) { + logInfo(">> typeHierarchy/subtypes"); + return computeAsync(monitor -> typeHierarchyHandler.getSubtypeItems(params, monitor)); + } + private CompletableFuture computeAsyncWithClientProgress(Function code) { return CompletableFutures.computeAsync((cc) -> { IProgressMonitor monitor = progressReporterManager.getProgressReporter(cc); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java new file mode 100644 index 0000000000..af57ab43a2 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java @@ -0,0 +1,282 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IOrdinaryClassFile; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.ITypeRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; +import org.eclipse.jdt.internal.core.JavaModelManager; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType; +import org.eclipse.jdt.ls.core.internal.JSONUtility; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.SymbolTag; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; + +public class TypeHierarchyHandler { + + public enum TypeHierarchyDirection { + Subtype, Supertype; + } + + private static class TypeHierarchyItemData { + private String handleIdentifier; + private String methodIdentifier; + private String methodName; + + public TypeHierarchyItemData(String handleIdentifier, String methodIdentifier, String methodName) { + this.handleIdentifier = handleIdentifier; + this.methodIdentifier = methodIdentifier; + this.methodName = methodName; + } + + private static TypeHierarchyItemData getTypeHierarchyItemData(Object data) { + if (data == null) { + return null; + } + Map map = JSONUtility.toModel(data, Map.class); + String handleIdentifier = map.get("element"); + String methodIdentifier = map.get("method"); + String methodName = map.get("method_name"); + return new TypeHierarchyItemData(handleIdentifier, methodIdentifier, methodName); + } + } + + public List prepareTypeHierarchy(TypeHierarchyPrepareParams params, IProgressMonitor monitor) { + if (params == null) { + return Collections.emptyList(); + } + TextDocumentIdentifier textDocument = params.getTextDocument(); + if (textDocument == null) { + return Collections.emptyList(); + } + Position position = params.getPosition(); + String uri = textDocument.getUri(); + return getTypeHierarchyItems(uri, position, monitor); + } + + private List getTypeHierarchyItems(String uri, Position position, IProgressMonitor monitor) { + if (uri == null || position == null) { + return Collections.emptyList(); + } + try { + IMember member = getMember(uri, position, monitor); + IMethod targetMethod = null; + if (member instanceof IMethod) { + targetMethod = (IMethod) member; + } + TypeHierarchyItem item = targetMethod == null ? TypeHierarchyHandler.toTypeHierarchyItem(member) : TypeHierarchyHandler.toTypeHierarchyItem(member, false, targetMethod); + if (item == null) { + return Collections.emptyList(); + } + return Arrays.asList(item); + } catch (JavaModelException e) { + return Collections.emptyList(); + } + } + + public List getSupertypeItems(TypeHierarchySupertypesParams params, IProgressMonitor monitor) { + return getTypeHierarchyItems(params.getItem(), TypeHierarchyDirection.Supertype, monitor); + } + + public List getSubtypeItems(TypeHierarchySubtypesParams params, IProgressMonitor monitor) { + return getTypeHierarchyItems(params.getItem(), TypeHierarchyDirection.Subtype, monitor); + } + + private List getTypeHierarchyItems(TypeHierarchyItem item, TypeHierarchyDirection direction, IProgressMonitor monitor) { + TypeHierarchyItemData data = TypeHierarchyItemData.getTypeHierarchyItemData(item.getData()); + if (data == null) { + return Collections.emptyList(); + } + IJavaElement element = JavaCore.create(data.handleIdentifier); + IMember member = null; + IMethod targetMethod = null; + if (data.methodIdentifier != null) { + targetMethod = (IMethod) JavaCore.create(data.methodIdentifier); + } + if (element instanceof IType || element instanceof IMethod) { + member = (IMember) element; + } else if (element instanceof IOrdinaryClassFile classFile) { + member = classFile.getType(); + } else { + return Collections.emptyList(); + } + return resolveTypeHierarchyItems(member, targetMethod, direction, monitor); + } + + private List resolveTypeHierarchyItems(IMember member, IMethod targetMethod, TypeHierarchyDirection direction, IProgressMonitor monitor) { + if (monitor.isCanceled()) { + return Collections.emptyList(); + } + IType type = null; + if (member instanceof IType) { + type = (IType) member; + } else { + type = member.getDeclaringType(); + } + try { + ITypeHierarchy typeHierarchy = null; + List items = new ArrayList<>(); + IType[] hierarchyTypes = null; + if (direction == TypeHierarchyDirection.Supertype) { + typeHierarchy = type.newSupertypeHierarchy(DefaultWorkingCopyOwner.PRIMARY, monitor); + hierarchyTypes = typeHierarchy.getSupertypes(type); + } else { + ICompilationUnit[] workingCopies = JavaModelManager.getJavaModelManager().getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, true); + typeHierarchy = type.newTypeHierarchy(workingCopies, monitor); + hierarchyTypes = typeHierarchy.getSubtypes(type); + } + for (IType hierarchyType : hierarchyTypes) { + if (monitor.isCanceled()) { + return Collections.emptyList(); + } + TypeHierarchyItem item = null; + if (targetMethod != null) { + IMethod[] matches = hierarchyType.findMethods(targetMethod); + boolean excludeMember = matches == null || matches.length == 0; + // Do not show java.lang.Object unless target method is based there + if (!excludeMember || !"java.lang.Object".equals(hierarchyType.getFullyQualifiedName())) { + item = TypeHierarchyHandler.toTypeHierarchyItem(excludeMember ? hierarchyType : matches[0], excludeMember, targetMethod); + } + } else { + item = TypeHierarchyHandler.toTypeHierarchyItem(hierarchyType); + } + if (item == null) { + continue; + } + items.add(item); + } + return items; + } catch (JavaModelException e) { + return Collections.emptyList(); + } + } + + private IMember getMember(String uri, Position position, IProgressMonitor monitor) throws JavaModelException { + IJavaElement typeElement = findTypeElement(JDTUtils.resolveTypeRoot(uri), position, monitor); + if (typeElement instanceof IType type) { + return type; + } else if (typeElement instanceof IMethod method) { + return method; + } else { + return null; + } + } + + private static IJavaElement findTypeElement(ITypeRoot unit, Position position, IProgressMonitor monitor) throws JavaModelException { + if (unit == null) { + return null; + } + IJavaElement element = JDTUtils.findElementAtSelection(unit, position.getLine(), position.getCharacter(), JavaLanguageServerPlugin.getPreferencesManager(), monitor); + if (element == null) { + if (unit instanceof IOrdinaryClassFile classFile) { + element = classFile.getType(); + } else if (unit instanceof ICompilationUnit) { + element = unit.findPrimaryType(); + } + } + return element; + } + + private static TypeHierarchyItem toTypeHierarchyItem(IMember member) throws JavaModelException { + return toTypeHierarchyItem(member, false, null); + } + + private static TypeHierarchyItem toTypeHierarchyItem(IMember member, boolean excludeMember, IMethod targetMethod) throws JavaModelException { + if (member == null) { + return null; + } + Location location = getLocation(member, LocationType.FULL_RANGE); + Location selectLocation = getLocation(member, LocationType.NAME_RANGE); + if (location == null || selectLocation == null) { + return null; + } + + Range range = location.getRange(); + String uri = location.getUri(); + Range selectionRange = selectLocation.getRange(); + + IType type = null; + if (member instanceof IType) { + type = (IType) member; + } else { + type = member.getDeclaringType(); + } + + String name = null; + String detail = null; + String fullyQualifiedName = type.getFullyQualifiedName(); + int index = fullyQualifiedName.lastIndexOf('.'); + if (index >= 1 && index < fullyQualifiedName.length() - 1 && !type.isAnonymous()) { + name = fullyQualifiedName.substring(index + 1); + detail = fullyQualifiedName.substring(0, index); + } else { + name = JDTUtils.getName(type); + IPackageFragment packageFragment = type.getPackageFragment(); + if (packageFragment != null) { + detail = packageFragment.getElementName(); + } + } + SymbolKind kind = excludeMember ? SymbolKind.Null : DocumentSymbolHandler.mapKind(type); + List tags = new ArrayList<>(); + if (JDTUtils.isDeprecated(member)) { + tags.add(SymbolTag.Deprecated); + } + Map data = new HashMap<>(); + data.put("element", member.getHandleIdentifier()); + if (targetMethod != null) { + data.put("method", targetMethod.getHandleIdentifier()); + data.put("method_name", targetMethod.getElementName()); + } else if (member instanceof IMethod) { + data.put("method", member.getHandleIdentifier()); + data.put("method_name", member.getElementName()); + } + TypeHierarchyItem item = new TypeHierarchyItem(name, kind, uri, range, selectionRange, detail); + item.setTags(tags); + item.setData(data); + return item; + } + + private static Location getLocation(IMember member, LocationType locationType) throws JavaModelException { + Location location = locationType.toLocation(member); + if (location == null && member.getClassFile() != null) { + location = JDTUtils.toLocation(member.getClassFile()); + } + return location; + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandlerTest.java new file mode 100644 index 0000000000..ca7219a027 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandlerTest.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.managers.AbstractInvisibleProjectBasedTest; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; +import org.junit.Before; +import org.junit.Test; + +public class TypeHierarchyHandlerTest extends AbstractInvisibleProjectBasedTest { + + private IProject fJProject; + private TypeHierarchyHandler fHandler; + + @Before + public void setup() throws Exception { + importProjects("maven/salut"); + fJProject = WorkspaceHelper.getProject("salut"); + fHandler = new TypeHierarchyHandler(); + } + + @Test + public void testSuperTypeHierarchy() throws Exception { + IProgressMonitor monitor = new NullProgressMonitor(); + TypeHierarchyPrepareParams params = new TypeHierarchyPrepareParams(); + String uriString = fJProject.getFile("src/main/java/org/sample/CallHierarchy.java").getLocationURI().toString(); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(uriString); + Position position = new Position(7, 27); + params.setTextDocument(identifier); + params.setPosition(position); + List items = fHandler.prepareTypeHierarchy(params, monitor); + assertNotNull(items); + assertEquals(1, items.size()); + assertEquals(items.get(0).getName(), "CallHierarchy$FooBuilder"); + TypeHierarchySupertypesParams supertypesParams = new TypeHierarchySupertypesParams(); + supertypesParams.setItem(items.get(0)); + List supertypesItems = fHandler.getSupertypeItems(supertypesParams, monitor); + assertNotNull(supertypesItems); + assertEquals(2, supertypesItems.size()); + assertEquals(supertypesItems.get(0).getName(), "Builder"); + assertEquals(supertypesItems.get(0).getKind(), SymbolKind.Interface); + assertEquals(supertypesItems.get(1).getName(), "Object"); + assertEquals(supertypesItems.get(1).getKind(), SymbolKind.Class); + } + + @Test + public void testSubTypeHierarchy() throws Exception { + IProgressMonitor monitor = new NullProgressMonitor(); + TypeHierarchyPrepareParams params = new TypeHierarchyPrepareParams(); + String uriString = fJProject.getFile("src/main/java/org/sample/CallHierarchy.java").getLocationURI().toString(); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(uriString); + Position position = new Position(2, 43); + params.setTextDocument(identifier); + params.setPosition(position); + List items = fHandler.prepareTypeHierarchy(params, monitor); + assertNotNull(items); + assertEquals(1, items.size()); + assertEquals(items.get(0).getName(), "Builder"); + TypeHierarchySubtypesParams supertypesParams = new TypeHierarchySubtypesParams(); + supertypesParams.setItem(items.get(0)); + List subtypesItems = fHandler.getSubtypeItems(supertypesParams, monitor); + assertNotNull(subtypesItems); + assertEquals(9, subtypesItems.size()); + } + + // https://github.com/redhat-developer/vscode-java/issues/2871 + @Test + public void testMultipleProjects() throws Exception { + importProjects("eclipse/gh2871"); + IProject project = WorkspaceHelper.getProject("project1"); + IProgressMonitor monitor = new NullProgressMonitor(); + TypeHierarchyPrepareParams params = new TypeHierarchyPrepareParams(); + String uriString = project.getFile("src/org/sample/First.java").getLocationURI().toString(); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(uriString); + Position position = new Position(1, 22); + params.setTextDocument(identifier); + params.setPosition(position); + List items = fHandler.prepareTypeHierarchy(params, monitor); + assertNotNull(items); + assertEquals(1, items.size()); + assertEquals("First", items.get(0).getName()); + TypeHierarchySubtypesParams supertypesParams = new TypeHierarchySubtypesParams(); + supertypesParams.setItem(items.get(0)); + List subtypesItems = fHandler.getSubtypeItems(supertypesParams, monitor); + assertNotNull(subtypesItems); + assertEquals(1, subtypesItems.size()); + assertEquals("Second", subtypesItems.get(0).getName()); + } + + @Test + public void testMethodHierarchy() throws Exception { + importProjects("maven/type-hierarchy"); + IProject project = WorkspaceHelper.getProject("type-hierarchy"); + String uriString = project.getFile("src/main/java/org/example/Zero.java").getLocationURI().toString(); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(uriString); + Position position = new Position(3, 17); // public void f[o]o() + TypeHierarchyPrepareParams zeroParams = new TypeHierarchyPrepareParams(); + zeroParams.setTextDocument(identifier); + zeroParams.setPosition(position); + List zeroItems = fHandler.prepareTypeHierarchy(zeroParams, monitor); + assertNotNull(zeroItems); + assertEquals(1, zeroItems.size()); + assertEquals("Zero", zeroItems.get(0).getName()); + assertEquals(SymbolKind.Class, zeroItems.get(0).getKind()); + + TypeHierarchySupertypesParams supertypesParams = new TypeHierarchySupertypesParams(); + supertypesParams.setItem(zeroItems.get(0)); + List supertypesItems = fHandler.getSupertypeItems(supertypesParams, monitor); + assertNotNull(supertypesItems); + // do not show java.lang.Object if target method isn't from there + assertEquals(0, supertypesItems.size()); + + TypeHierarchySubtypesParams subtypesParams = new TypeHierarchySubtypesParams(); + subtypesParams.setItem(zeroItems.get(0)); + List subtypesItems = fHandler.getSubtypeItems(subtypesParams, monitor); + assertNotNull(subtypesItems); + assertEquals(SymbolKind.Class, subtypesItems.get(0).getKind()); // one + assertEquals("One", subtypesItems.get(0).getName()); // one + assertEquals(SymbolKind.Null, subtypesItems.get(1).getKind()); // two + assertEquals("Two", subtypesItems.get(1).getName()); // two + + TypeHierarchyItem one = subtypesItems.get(0); + TypeHierarchyItem two = subtypesItems.get(1); + subtypesParams.setItem(one); + subtypesItems = fHandler.getSubtypeItems(subtypesParams, monitor); + assertNotNull(subtypesItems); + assertEquals(SymbolKind.Null, subtypesItems.get(1).getKind()); // three + assertEquals("Three", subtypesItems.get(1).getName()); // three + assertEquals(SymbolKind.Class, subtypesItems.get(0).getKind()); // four + assertEquals("Four", subtypesItems.get(0).getName()); // four + + subtypesParams.setItem(two); + subtypesItems = fHandler.getSubtypeItems(subtypesParams, monitor); + assertNotNull(subtypesItems); + assertEquals(SymbolKind.Null, subtypesItems.get(0).getKind()); // five + assertEquals("Five", subtypesItems.get(0).getName()); // five + assertEquals(SymbolKind.Class, subtypesItems.get(1).getKind()); // six + assertEquals("Six", subtypesItems.get(1).getName()); // six + } +}