diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java index 2a67ec3098..5489b9a987 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java @@ -355,6 +355,13 @@ ISourceRange getRange(IJavaElement element) throws JavaModelException { }; /* default */ abstract ISourceRange getRange(IJavaElement element) throws JavaModelException; + + /** + * Sugar for {@link JDTUtils#toLocation(IJavaElement, LocationType)}. + */ + public Location toLocation(IJavaElement element) throws JavaModelException { + return JDTUtils.toLocation(element, this); + } } /** diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java index db1767349f..adada3b433 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java @@ -12,12 +12,14 @@ import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; +import static org.eclipse.jdt.core.IJavaElement.CLASS_FILE; import static org.eclipse.jdt.core.IJavaElement.COMPILATION_UNIT; import static org.eclipse.jdt.core.IJavaElement.FIELD; import static org.eclipse.jdt.core.IJavaElement.METHOD; import static org.eclipse.jdt.core.IJavaElement.PACKAGE_DECLARATION; import static org.eclipse.jdt.core.IJavaElement.TYPE; import static org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType.FULL_RANGE; +import static org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType.NAME_RANGE; import static org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin.logInfo; import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.ALL_DEFAULT; import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.M_APP_RETURNTYPE; @@ -31,9 +33,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IClassFile; +import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IParent; @@ -41,6 +46,7 @@ import org.eclipse.jdt.core.ITypeRoot; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.ResourceUtils; import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels; @@ -109,7 +115,7 @@ private void collectChildren(ITypeRoot unit, IJavaElement[] elements, ArrayList< SymbolInformation si = new SymbolInformation(); String name = JavaElementLabels.getElementLabel(element, JavaElementLabels.ALL_DEFAULT); si.setName(name == null ? element.getElementName() : name); - si.setKind(mapKind(element)); + si.setKind(getSymbolKind(element)); if (element.getParent() != null) { si.setContainerName(element.getParent().getElementName()); } @@ -147,7 +153,7 @@ private DocumentSymbol toDocumentSymbol(IJavaElement unit, IProgressMonitor moni symbol.setName(name); symbol.setRange(getRange(unit)); symbol.setSelectionRange(getSelectionRange(unit)); - symbol.setKind(mapKind(unit)); + symbol.setKind(getSymbolKind(unit)); symbol.setDeprecated(isDeprecated(unit)); symbol.setDetail(getDetail(unit, name)); if (unit instanceof IParent) { @@ -165,34 +171,6 @@ private DocumentSymbol toDocumentSymbol(IJavaElement unit, IProgressMonitor moni return symbol; } - private String getName(IJavaElement element) { - String name = JavaElementLabels.getElementLabel(element, ALL_DEFAULT); - return name == null ? element.getElementName() : name; - } - - private Range getRange(IJavaElement element) throws JavaModelException { - return JDTUtils.toLocation(element, FULL_RANGE).getRange(); - } - - private Range getSelectionRange(IJavaElement element) throws JavaModelException { - return JDTUtils.toLocation(element).getRange(); - } - - private boolean isDeprecated(IJavaElement element) throws JavaModelException { - if (element instanceof ITypeRoot) { - return Flags.isDeprecated(((ITypeRoot) element).findPrimaryType().getFlags()); - } - return false; - } - - private String getDetail(IJavaElement element, String name) { - String nameWithDetails = JavaElementLabels.getElementLabel(element, ALL_DEFAULT | M_APP_RETURNTYPE | ROOT_VARIABLE); - if (nameWithDetails != null && nameWithDetails.startsWith(name)) { - return nameWithDetails.substring(name.length()); - } - return ""; - } - private IJavaElement[] filter(IJavaElement[] elements) { return Stream.of(elements) .filter(e -> (!isInitializer(e) && !isSyntheticElement(e))) @@ -224,7 +202,119 @@ private boolean isSyntheticElement(IJavaElement element) { } } - public static SymbolKind mapKind(IJavaElement element) { + /** + * Returns with the human readable name of the element. For types with type + * arguments, it is {@code Comparable} instead of {@code Comparable}. First, + * this method tries to retrieve the + * {@link JavaElementLabels#getElementLabel(IJavaElement, long) label} of the + * element, then falls back to {@link IJavaElement#getElementName() element + * name}. Returns {@code null} if the argument does not have a name. + */ + public static String getName(IJavaElement element) { + Assert.isNotNull(element, "element"); + String name = JavaElementLabels.getElementLabel(element, ALL_DEFAULT); + return name == null ? element.getElementName() : name; + } + + /** + * The location, encapsulating the range enclosing this symbol not including + * leading/trailing whitespace but everything else like comments. + */ + public static Location getFullLocation(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + return getLocation(element, FULL_RANGE); + } + + /** + * The location, including the range that should be selected and revealed when + * this symbol is being picked, e.g the name of a method. Always contained by + * the range of the {@link #getFullLocation(IJavaElement) full location}. + */ + public static Location getSelectionLocation(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + return getLocation(element, NAME_RANGE); + } + + /** + * Gets the location of the Java {@code element} based on the desired + * {@code locationType}. + */ + private static Location getLocation(IJavaElement element, LocationType locationType) throws JavaModelException { + Assert.isNotNull(element, "element"); + Assert.isNotNull(locationType, "locationType"); + Location location = locationType.toLocation(element); + if (location == null && element instanceof IType) { + IType type = (IType) element; + ICompilationUnit unit = (ICompilationUnit) type.getAncestor(COMPILATION_UNIT); + IClassFile classFile = (IClassFile) type.getAncestor(CLASS_FILE); + if (unit != null || (classFile != null && classFile.getSourceRange() != null)) { + return locationType.toLocation(type); + } + if (type instanceof IMember && ((IMember) type).getClassFile() != null) { + return JDTUtils.toLocation(((IMember) type).getClassFile()); + } + } + return location; + } + + /** + * The range enclosing this symbol not including leading/trailing whitespace but + * everything else like comments. + * + * @see DocumentSymbolHandler#getFullLocation(IJavaElement) + */ + public static Range getRange(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + return getLocation(element, FULL_RANGE).getRange(); + } + + /** + * The range that should be selected and revealed when this symbol is being + * picked, e.g the name of a method. Always contained by the range of the + * {@link #getFullLocation(IJavaElement) full location}. + * + * @see DocumentSymbolHandler#getSelectionLocation(IJavaElement) + */ + public static Range getSelectionRange(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + return getLocation(element, NAME_RANGE).getRange(); + } + + /** + * {@code true} if the element is deprecated. Otherwise, {@code false}. + */ + public static boolean isDeprecated(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + if (element instanceof ITypeRoot) { + return Flags.isDeprecated(((ITypeRoot) element).findPrimaryType().getFlags()); + } else if (element instanceof IMember) { + return Flags.isDeprecated(((IMember) element).getFlags()); + } + return false; + } + + /** + * Returns with the details of the document symbol. This is usually the type + * information of a member. For methods, this is the type information of the + * return type. Can return with an empty string, but never {@code null}. + * + * @see DocumentSymbolHandler#getName(IJavaElement) + */ + public static String getDetail(IJavaElement element, String name) { + Assert.isNotNull(element, "element"); + Assert.isNotNull(name, "name"); + String nameWithDetails = JavaElementLabels.getElementLabel(element, ALL_DEFAULT | M_APP_RETURNTYPE | ROOT_VARIABLE); + if (nameWithDetails != null && nameWithDetails.startsWith(name)) { + return nameWithDetails.substring(name.length()); + } + return ""; + } + + /** + * Returns with the document symbol {@code SymbolKind kind} for the Java + * element. + */ + public static SymbolKind getSymbolKind(IJavaElement element) { switch (element.getElementType()) { case IJavaElement.ANNOTATION: return SymbolKind.Property; // TODO: find a better mapping @@ -247,7 +337,14 @@ public static SymbolKind mapKind(IJavaElement element) { return SymbolKind.Package; case IJavaElement.TYPE: try { - return (((IType)element).isInterface() ? SymbolKind.Interface : SymbolKind.Class); + IType type = (IType) element; + if (type.isEnum()) { + return SymbolKind.Enum; + } else if (type.isInterface()) { + return SymbolKind.Interface; + } else { + return SymbolKind.Class; + } } catch (JavaModelException e) { return SymbolKind.Class; } 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 7835be8374..051eebc60f 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 @@ -205,6 +205,8 @@ InitializeResult initialize(InitializeParams param) { wsCapabilities.setWorkspaceFolders(wsFoldersOptions); capabilities.setWorkspace(wsCapabilities); + capabilities.setTypeHierarchy(true); + result.setCapabilities(capabilities); return result; } 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 0d5bf7f1df..be2b0235cf 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 @@ -730,6 +730,20 @@ public CompletableFuture> implementation(TextDocumentPo return computeAsyncWithClientProgress((monitor) -> new ImplementationsHandler(preferenceManager).findImplementations(position, monitor)); } + @Override + public CompletableFuture subTypes(TextDocumentPositionParams params) { + logInfo(">> textDocument/subTypes"); + TypeHierarchyHandler handler = new TypeHierarchyHandler(preferenceManager); + return computeAsyncWithClientProgress(monitor -> handler.getSubTypes(params)); + } + + @Override + public CompletableFuture superTypes(TextDocumentPositionParams params) { + logInfo(">> textDocument/superTypes"); + TypeHierarchyHandler handler = new TypeHierarchyHandler(preferenceManager); + return computeAsyncWithClientProgress(monitor -> handler.getSuperTypes(params)); + } + public void sendStatus(ServiceStatus serverStatus, String status) { if (client != null) { client.sendStatus(serverStatus, status); 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..f380fb8b42 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandler.java @@ -0,0 +1,340 @@ +/******************************************************************************* + * Copyright (c) 2018 TypeFox and others. + * 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: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static com.google.common.collect.Lists.newArrayList; +import static java.util.stream.Collectors.toList; +import static org.eclipse.jdt.core.ICompilationUnit.NO_AST; +import static org.eclipse.jdt.core.IJavaElement.CLASS_FILE; +import static org.eclipse.jdt.core.IJavaElement.COMPILATION_UNIT; +import static org.eclipse.jdt.core.IJavaElement.METHOD; +import static org.eclipse.jdt.core.IJavaElement.TYPE; +import static org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin.logException; +import static org.eclipse.jdt.ls.core.internal.handlers.DocumentSymbolHandler.getName; +import static org.eclipse.jdt.ls.core.internal.handlers.DocumentSymbolHandler.getSelectionRange; +import static org.eclipse.jdt.ls.core.internal.handlers.DocumentSymbolHandler.getSymbolKind; +import static org.eclipse.jdt.ls.core.internal.handlers.DocumentSymbolHandler.isDeprecated; +import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.ALL_DEFAULT; +import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.DEFAULT_QUALIFIED; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +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.JavaModelException; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels; +import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; + +/** + * Handler for serving the {@code textDocument/superTypes} and the + * {@code textDocument/subTypes} methods. + * + * Calculates super- and subtype hierarchical information based on text document + * positions. The super- and the subtype parentage information is calculated + * lazily, on demand. + * + */ +public class TypeHierarchyHandler { + + //@formatter:off + protected static Map> SUPPORTED_TYPES = ImmutableMap.>builder() + .put(TYPE, type -> (IType) type) + .put(METHOD, method -> ((IMethod) method).getDeclaringType()) + .build(); + //@formatter:on + + protected PreferenceManager preferenceManager; + protected Supplier enabled; + + public TypeHierarchyHandler(PreferenceManager preferenceManager) { + this.preferenceManager = preferenceManager; + enabled = Suppliers.memoize(() -> this.preferenceManager.getClientPreferences().isTypeHierarchySupported()); + } + + public DocumentSymbol getSubTypes(DocumentSymbol symbol) { + return getTypeHierarchy(toParams(symbol), TypeHierarchyStrategy.SUB, new NullProgressMonitor()); + } + + public DocumentSymbol getSubTypes(TextDocumentPositionParams params) { + return getSubTypes(params, new NullProgressMonitor()); + } + + public DocumentSymbol getSubTypes(TextDocumentPositionParams params, IProgressMonitor monitor) { + return getTypeHierarchy(params, TypeHierarchyStrategy.SUB, monitor); + } + + public DocumentSymbol getSuperTypes(DocumentSymbol symbol) { + return getTypeHierarchy(toParams(symbol), TypeHierarchyStrategy.SUPER, new NullProgressMonitor()); + } + + public DocumentSymbol getSuperTypes(TextDocumentPositionParams params) { + return getSuperTypes(params, new NullProgressMonitor()); + } + + public DocumentSymbol getSuperTypes(TextDocumentPositionParams params, IProgressMonitor monitor) { + return getTypeHierarchy(params, TypeHierarchyStrategy.SUPER, monitor); + } + + protected DocumentSymbol getTypeHierarchy(TextDocumentPositionParams params, TypeHierarchyStrategy strategy, IProgressMonitor monitor) { + if (!enabled.get()) { + return null; + } + SubMonitor subMonitor = SubMonitor.convert(monitor, 3); + String uri = params.getTextDocument().getUri(); + ITypeRoot root = JDTUtils.resolveTypeRoot(uri); + if (root == null) { + return null; + } + try { + if (root instanceof ICompilationUnit) { + ICompilationUnit unit = (ICompilationUnit) root; + if (root.getResource() == null) { + return null; + } + reconcile(unit, subMonitor.newChild(1)); + } + + int line = params.getPosition().getLine(); + int character = params.getPosition().getCharacter(); + IJavaElement selectedElement = JDTUtils.findElementAtSelection(root, line, character, preferenceManager, subMonitor.newChild(1)); + if (!isSupportedType(selectedElement)) { + selectedElement = getFallbackElement(root, selectedElement); + if (!isSupportedType(selectedElement)) { + return null; + } + } + + IType selectedType = getType(selectedElement); + if (selectedType != null) { + // XXX: Can we cache the calculated hierarchy for sub subsequent calls? How and when to invalidate this cache then? + // Let's go with the naive way first, and enable some caching when we hit a performance issue. + ITypeHierarchy hierarchy = strategy.getTypeHierarchy(selectedType, subMonitor.newChild(1)); + return toDocumentSymbol(hierarchy, strategy); + } + } catch (JavaModelException e) { + logException("Error when calculating the " + strategy.name().toLowerCase() + "type hierarchy for " + uri + ". [" + params + "].", e); + } + return null; + } + + /** + * If the {@link IJavaElement selectedElement} argument is not + * {@link #isSupportedType(IJavaElement) supported}, it returns with the primary + * type from the {@code root} argument. Otherwise, returns the + * {@code selectedElement} argument. + */ + private IJavaElement getFallbackElement(ITypeRoot root, IJavaElement selectedElement) { + if (!isSupportedType(selectedElement)) { + int rootType = root.getElementType(); + if (rootType == CLASS_FILE && root instanceof IOrdinaryClassFile) { + selectedElement = ((IOrdinaryClassFile) root).getType(); + } else if (rootType == COMPILATION_UNIT) { + selectedElement = ((ICompilationUnit) root).findPrimaryType(); + } + } + return isSupportedType(selectedElement) ? selectedElement : null; + } + + private void reconcile(ICompilationUnit unit, IProgressMonitor monitor) throws JavaModelException { + unit.reconcile(NO_AST, false, null, monitor); + } + + /** + * {@code true} if the {@code element} argument is not {@code null} and its + * {@link IJavaElement#getElementType() type} is covered by this handler. + */ + private boolean isSupportedType(IJavaElement element) { + return element != null && SUPPORTED_TYPES.keySet().contains(element.getElementType()); + } + + /** + * Returns the {@link IType type} from the {@link IJavaElement element} argument + * if it is supported. Otherwise, returns {@code null}. + */ + private IType getType(IJavaElement element) { + return SUPPORTED_TYPES.getOrDefault(element.getElementType(), type -> null).apply(element); + } + + /** + * Maps the type argument into the corresponding document symbol. Returns with + * {@code null} if the type is not supported. The + * {@link DocumentSymbol#getChildren() children} will be initialized, but will + * be empty after calling this method. + */ + private DocumentSymbol toDocumentSymbol(IType type) { + if (!isSupportedType(type)) { + return null; + } + try { + Location fullLocation = DocumentSymbolHandler.getFullLocation(type); + Range range = fullLocation.getRange(); + String uri = fullLocation.getUri(); + DocumentSymbol symbol = new DocumentSymbol(); + symbol.setName(getName(type)); + symbol.setKind(getSymbolKind(type)); + symbol.setRange(range); + symbol.setSelectionRange(getSelectionRange(type)); + symbol.setUri(uri); + symbol.setDetail(getDetail(type)); + symbol.setDeprecated(isDeprecated(type)); + symbol.setChildren(newArrayList()); + return symbol; + } catch (JavaModelException e) { + logException("Error when mapping type " + type + " into a document symbol.", e); + } + return null; + } + + /** + * Maps the {@link ITypeHierarchy type hierarchy} argument into a two level deep + * {@link DocumentSymbol document symbol}. + */ + private DocumentSymbol toDocumentSymbol(ITypeHierarchy hierarchy, TypeHierarchyStrategy strategy) throws JavaModelException { + if (hierarchy == null) { + return null; + } + IType type = hierarchy.getType(); + DocumentSymbol symbol = toDocumentSymbol(type); + if (isValid(symbol)) { + //@formatter:off + symbol.setChildren(Stream.of(strategy.getChildren(type, hierarchy)) + .map(childType -> toDocumentSymbol(childType)) + .filter(childSymbol -> isValid(childSymbol)) + .collect(toList())); + //@formatter:on + } + return symbol; + } + + /** + * The FQN of the container package of the {@code type} argument. + */ + private String getDetail(IType type) { + IPackageFragment packageFragment = type.getPackageFragment(); + if (packageFragment != null) { + String name = JavaElementLabels.getElementLabel(packageFragment, ALL_DEFAULT); + return name == null ? packageFragment.getElementName() : name; + } + String fqnName = JavaElementLabels.getElementLabel(type, DEFAULT_QUALIFIED); + if (fqnName != null) { + String name = getName(type); + if (name != null && fqnName.endsWith(name)) { + return fqnName.substring(0, fqnName.length() - (name.length() + 1)); + } + } + return null; + } + + // XXX: https://github.com/eclipse/lsp4j/issues/264 + private boolean isValid(DocumentSymbol symbol) { + //@formatter:off + return symbol != null + && symbol.getName() != null + && symbol.getKind() != null + && symbol.getRange() != null + && symbol.getSelectionRange() != null + && symbol.getUri() != null; // XXX: Note, this is not mandatory in general but for type hierarchy, it is. + //@formatter:on + } + + /** + * Sugar for converting a {@link DocumentSymbol symbol} into a + * {@link TextDocumentPositionParams text document position} parameter. + */ + private TextDocumentPositionParams toParams(DocumentSymbol symbol) { + Assert.isNotNull(symbol, "symbol"); + Assert.isNotNull(symbol.getUri(), "symbol.uri"); + String uri = symbol.getUri(); + Position start = symbol.getSelectionRange().getStart(); + //@formatter:off + return new TextDocumentPositionParams( + new TextDocumentIdentifier(uri), + new Position(start.getLine(), start.getCharacter())); + //@formatter:on + } + + /** + * Strategies for collecting super- and subtypes. + */ + protected static enum TypeHierarchyStrategy { + + SUB { + + @Override + protected ITypeHierarchy getTypeHierarchy(IType type, IProgressMonitor monitor) throws JavaModelException { + return type.newTypeHierarchy(monitor); + } + + @Override + protected IType[] getChildren(IType type, ITypeHierarchy hierarchy) throws JavaModelException { + IType[] children = hierarchy.getSubtypes(type); + return children == null ? EMPTY : children; + } + + }, + + SUPER { + + @Override + protected ITypeHierarchy getTypeHierarchy(IType type, IProgressMonitor monitor) throws JavaModelException { + return type.newSupertypeHierarchy(monitor); + } + + @Override + protected IType[] getChildren(IType type, ITypeHierarchy hierarchy) throws JavaModelException { + IType[] children = hierarchy.getSupertypes(type); + return children == null ? EMPTY : children; + } + + }; + + /** + * Shared empty types. + */ + private static final IType[] EMPTY = new IType[0]; + + /** + * Returns with the type hierarchy for the given type argument. + */ + protected abstract ITypeHierarchy getTypeHierarchy(IType type, IProgressMonitor monitor) throws JavaModelException; + + /** + * Returns with the children (super-, or subtype) of the type from the + * hierarchy. Never {@code null}. + */ + protected abstract IType[] getChildren(IType type, ITypeHierarchy hierarchy) throws JavaModelException; + + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java index 5913269aaa..5ab806bab1 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java @@ -190,4 +190,18 @@ public boolean isHierarchicalDocumentSymbolSupported() { && capabilities.getTextDocument().getDocumentSymbol().getHierarchicalDocumentSymbolSupport().booleanValue(); //@formatter:on } + + /** + * {@code true} if the client has explicitly set the + * {@code textDocument.typeHierarchyCapabilities.typeHierarchy} to {@code true} + * when initializing the LS. Otherwise, {@code false}. + */ + public boolean isTypeHierarchySupported() { + //@formatter:off + return v3supported + && capabilities.getTextDocument().getTypeHierarchyCapabilities() != null + && capabilities.getTextDocument().getTypeHierarchyCapabilities().getTypeHierarchy() != null + && capabilities.getTextDocument().getTypeHierarchyCapabilities().getTypeHierarchy().booleanValue(); + //@formatter:on + } } diff --git a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target index 49c3f9ac49..4e38a4bd6a 100644 --- a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target +++ b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target @@ -20,7 +20,7 @@ - + @@ -33,14 +33,18 @@ - - + + - - + + + + + + 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..fec8bbfda5 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/TypeHierarchyHandlerTest.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * Copyright (c) 2018 TypeFox and others. + * 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: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static org.eclipse.lsp4j.SymbolKind.Class; +import static org.eclipse.lsp4j.SymbolKind.Enum; +import static org.eclipse.lsp4j.SymbolKind.Interface; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.stream.StreamSupport; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ls.core.internal.ClassFileUtil; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.Iterables; + +public class TypeHierarchyHandlerTest extends AbstractProjectsManagerBasedTest { + + private IProject project; + + @Before + public void setup() throws Exception { + importProjects("maven/salut"); + project = WorkspaceHelper.getProject("salut"); + when(preferenceManager.getClientPreferences().isTypeHierarchySupported()).thenReturn(true); + } + + @Test + public void subTypeInJar_disabled() throws Exception { + try { + when(preferenceManager.getClientPreferences().isTypeHierarchySupported()).thenReturn(false); + // Line 99 from `WordUtils` + // public static String wrap(final String str, final int wrapLength) { + String uri = getUriForClassName("org.apache.commons.lang3.text.WordUtils"); + assertNull(getSubTypes(uri, 98, 19)); + } finally { + when(preferenceManager.getClientPreferences().isTypeHierarchySupported()).thenReturn(true); + } + } + + @Test + public void subTypeInJar_type() throws Exception { + // Line 99 from `WordUtils` + // public static String wrap(final String str, final int wrapLength) { + String uri = getUriForClassName("org.apache.commons.lang3.text.WordUtils"); + doAssert(getSubTypes(uri, 98, 19), "String", Class); + } + + @Test + public void subTypeInJar_variable() throws Exception { + // Line 284 from `WordUtils`. + // final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32); + String uri = getUriForClassName("org.apache.commons.lang3.text.WordUtils"); + doAssert(getSubTypes(uri, 283, 29), "WordUtils", Class); + } + + @Test + public void subTypeInJar_method() throws Exception { + // Line 99 from `WordUtils` + // public static String wrap(final String str, final int wrapLength) { + String uri = getUriForClassName("org.apache.commons.lang3.text.WordUtils"); + doAssert(getSubTypes(uri, 98, 26), "WordUtils", Class); + } + + @Test + public void subTypeInJar_typeArgument() throws Exception { + // Line 324 from `TypeUtils` + // final Map, Type> typeVarAssigns) { + String uri = getUriForClassName("org.apache.commons.lang3.reflect.TypeUtils"); + doAssert(getSubTypes(uri, 323, 22), "TypeVariable", Interface); + } + + @Test + public void subTypeInJar_noSelectedJavaElement() throws Exception { + // Line 324 from `TypeUtils` + // final Map, Type> typeVarAssigns) { + String uri = getUriForClassName("org.apache.commons.lang3.reflect.TypeUtils"); + doAssert(getSubTypes(uri, 0, 0), "TypeUtils", Class); + } + + @Test + public void superTypeInJar_typeArgument() throws Exception { + // Line 324 from `TypeUtils` + // final Map, Type> typeVarAssigns) { + String uri = getUriForClassName("org.apache.commons.lang3.reflect.TypeUtils"); + DocumentSymbol typeVariableNode = doAssert(getSuperTypes(uri, 323, 22), "TypeVariable", Interface); + List superTypes = getSuperTypes(typeVariableNode).getChildren(); + assertEquals(2, superTypes.size()); + doAssertContains(superTypes, "AnnotatedElement", Interface); + doAssertContains(superTypes, "Type", Interface); + } + + @Test + public void superTypeInJar_ctor() throws Exception { + // Line 109 from `CompareToBuilder` + // public CompareToBuilder() { + String uri = getUriForClassName("org.apache.commons.lang3.builder.CompareToBuilder"); + DocumentSymbol typeVariableNode = doAssert(getSuperTypes(uri, 108, 11), "CompareToBuilder", Class); + List superTypes = getSuperTypes(typeVariableNode).getChildren(); + assertEquals(2, superTypes.size()); + doAssertContains(superTypes, "Object", Class); + doAssertContains(superTypes, "Builder", Interface); + } + + @Test + public void superTypeInJar_interfaceDoesNotExtendObject() throws Exception { + // Line 79 from `Builder` + //public interface Builder { + String uri = getUriForClassName("org.apache.commons.lang3.builder.Builder"); + DocumentSymbol typeVariableNode = doAssert(getSuperTypes(uri, 78, 17), "Builder", Interface); + List superTypes = getSuperTypes(typeVariableNode).getChildren(); + assertEquals(0, superTypes.size()); + } + + @Test + public void superTypeInJar_enum() throws Exception { + // Line 303 from `JavaVersion` + // JAVA_1_8(1.8f, "1.8"), + String uri = getUriForClassName("org.apache.commons.lang3.JavaVersion"); + DocumentSymbol enumNode = doAssert(getSuperTypes(uri, 302, 4), "JavaVersion", Enum); + List superTypes = getSuperTypes(enumNode).getChildren(); + assertEquals(1, superTypes.size()); + doAssertContains(superTypes, "Enum>", Class); + List superSuperTypes = getSuperTypes(superTypes.iterator().next()).getChildren(); + assertEquals(3, superSuperTypes.size()); + doAssertContains(superSuperTypes, "Object", Class); + doAssertContains(superSuperTypes, "Comparable", Interface); + doAssertContains(superSuperTypes, "Serializable", Interface); + } + + /** + * Asserts the node and returns with the argument if valid. Otherwise, throws an + * exception. + */ + private DocumentSymbol doAssert(DocumentSymbol node, String expectedName, SymbolKind expectedKind) { + assertNotNull(node); + assertEquals("Unexpected name in: " + node.toString(), expectedName, node.getName()); + assertEquals("Unexpected symbol kind in: " + node.toString(), expectedKind, node.getKind()); + return node; + } + + private DocumentSymbol doAssertContains(Iterable nodes, String expectedName, SymbolKind expectedKind) { + assertNotNull(nodes); + //@formatter:off + return StreamSupport.stream(nodes.spliterator(), false) + .filter(node -> node.getName().equals(expectedName)) + .filter(node -> node.getKind().equals(expectedKind)) + .findFirst() + .orElseThrow(() -> new AssertionError( + new StringBuilder("Cannot find node with name: '") + .append(expectedName) + .append("' and kind: '") + .append(expectedKind) + .append("' in ") + .append(Iterables.toString(nodes)) + .append(".") + .toString())); + //@formatter:on + + } + + private String getUriForClassName(String className) throws JavaModelException { + return ClassFileUtil.getURI(project, className); + } + + private DocumentSymbol getSubTypes(String uri, int line, int character) { + return new TypeHierarchyHandler(preferenceManager).getSubTypes(newParams(uri, line, character)); + } + + private DocumentSymbol getSuperTypes(DocumentSymbol node) { + return new TypeHierarchyHandler(preferenceManager).getSuperTypes(node); + } + + private DocumentSymbol getSuperTypes(String uri, int line, int character) { + return new TypeHierarchyHandler(preferenceManager).getSuperTypes(newParams(uri, line, character)); + } + + private TextDocumentPositionParams newParams(String uri, int line, int character) { + return new TextDocumentPositionParams(new TextDocumentIdentifier(uri), new Position(line, character)); + } +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferencesTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferencesTest.java index 280812ac55..28ad154935 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferencesTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferencesTest.java @@ -24,6 +24,7 @@ import org.eclipse.lsp4j.RenameCapabilities; import org.eclipse.lsp4j.SignatureHelpCapabilities; import org.eclipse.lsp4j.TextDocumentClientCapabilities; +import org.eclipse.lsp4j.TypeHierarchyCapabilities; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -144,4 +145,19 @@ public void testIsHierarchicalDocumentSymbolSupported() throws Exception { when(text.getDocumentSymbol()).thenReturn(capabilities); assertTrue(prefs.isHierarchicalDocumentSymbolSupported()); } + + @Test + public void testIsTypeHierarchySupported() throws Exception { + TypeHierarchyCapabilities capabilities = new TypeHierarchyCapabilities(); + assertFalse(prefs.isTypeHierarchySupported()); + when(text.getTypeHierarchyCapabilities()).thenReturn(capabilities); + assertFalse(prefs.isTypeHierarchySupported()); + capabilities.setTypeHierarchy(false); + when(text.getTypeHierarchyCapabilities()).thenReturn(capabilities); + assertFalse(prefs.isTypeHierarchySupported()); + capabilities.setTypeHierarchy(true); + when(text.getTypeHierarchyCapabilities()).thenReturn(capabilities); + assertTrue(prefs.isTypeHierarchySupported()); + } + }