blob: 6a4da26046b19a27db5c732d981d112f27eb1bac [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2019 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
* Michał Niewrzał (Rogue Wave Software Inc.) - hyperlink range detection
* Lucas Bullen (Red Hat Inc.) - [Bug 517428] Requests sent before initialization
*******************************************************************************/
package org.eclipse.lsp4e.operations.declaration;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TypeDefinitionRegistrationOptions;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
public class OpenDeclarationHyperlinkDetector extends AbstractHyperlinkDetector {
@Override
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
final IDocument document = textViewer.getDocument();
TextDocumentPositionParams params;
try {
URI uri = LSPEclipseUtils.toUri(document);
if (uri == null) {
return null;
}
params = new TextDocumentPositionParams(
new TextDocumentIdentifier(uri.toString()),
LSPEclipseUtils.toPosition(region.getOffset(), document));
} catch (BadLocationException e1) {
LanguageServerPlugin.logError(e1);
return null;
}
IRegion r = findWord(textViewer.getDocument(), region.getOffset());
final IRegion linkRegion = r != null ? r : region;
Map<Either<Location, LocationLink>,LSBasedHyperlink> allLinks = Collections.synchronizedMap(new LinkedHashMap<>());
try {
// Collect definitions
Collection<CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>>> allFutures = Collections.synchronizedCollection(new ArrayList<>());
CompletableFuture.allOf(
LanguageServiceAccessor
.getLanguageServers(textViewer.getDocument(), capabilities -> LSPEclipseUtils.hasCapability(capabilities.getDefinitionProvider()))
.thenAcceptAsync(languageServers ->
languageServers.stream().map(ls -> ls.getTextDocumentService().definition(LSPEclipseUtils.toDefinitionParams(params))).forEach(allFutures::add)
),
LanguageServiceAccessor
.getLanguageServers(textViewer.getDocument(), OpenDeclarationHyperlinkDetector::isTypeDefinitionProvider)
.thenAcceptAsync(languageServers ->
languageServers.stream().map(ls -> ls.getTextDocumentService().typeDefinition(LSPEclipseUtils.toTypeDefinitionParams(params))).forEach(allFutures::add)
)
).thenCompose(theVoid ->
CompletableFuture.allOf(allFutures.stream().map(future ->
future.thenAccept(locations -> {
Collection<LSBasedHyperlink> links = toHyperlinks(document, linkRegion, locations);
synchronized (allLinks) {
links.forEach(link -> {
allLinks.putIfAbsent(link.getLocation(), link);
});
}
})).toArray(CompletableFuture[]::new))
).get(500, TimeUnit.MILLISECONDS);
} catch (ExecutionException | TimeoutException e) {
LanguageServerPlugin.logError(e);
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
}
if (allLinks.isEmpty()) {
return null;
}
return allLinks.values().toArray(new IHyperlink[allLinks.size()]);
}
/**
* Fill the given Eclipse links by using the given LSP locations
*
* @param document
* the document
* @param linkRegion
* the link region
* @param locations
* the LSP locations
* @param allLinks
* the Eclipse links to update
*/
private static Collection<LSBasedHyperlink> toHyperlinks(final IDocument document, final IRegion linkRegion,
Either<List<? extends Location>, List<? extends LocationLink>> locations) {
if (locations == null) {
return Collections.emptyList();
}
if (locations.isLeft()) {
return locations.getLeft().stream()
.filter(Objects::nonNull)
.map(location -> new LSBasedHyperlink(location, linkRegion))
.collect(Collectors.toList());
} else if (locations.isRight()) {
return locations.getRight().stream().filter(Objects::nonNull).map(locationLink -> {
IRegion selectionRegion = linkRegion;
Range originSelectionRange = locationLink.getOriginSelectionRange();
if (originSelectionRange != null) {
try {
int offset = LSPEclipseUtils
.toOffset(originSelectionRange.getStart(), document);
int endOffset = LSPEclipseUtils
.toOffset(originSelectionRange.getEnd(), document);
selectionRegion = new Region(offset, endOffset - offset);
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e.getMessage(), e);
}
}
return new LSBasedHyperlink(locationLink, selectionRegion);
}).collect(Collectors.toList());
}
return Collections.emptyList();
}
private static boolean isTypeDefinitionProvider(ServerCapabilities capabilities) {
Either<Boolean, TypeDefinitionRegistrationOptions> typeDefinitionProvider = capabilities.getTypeDefinitionProvider();
if (typeDefinitionProvider == null) {
return false;
}
if (typeDefinitionProvider.isLeft()) {
return Boolean.TRUE.equals(typeDefinitionProvider.getLeft());
} else if (typeDefinitionProvider.isRight()) {
return true;
}
return false;
}
/**
* This method is only a workaround for missing range value (which can be used
* to highlight hyperlink) in LSP 'definition' response.
*
* Should be removed when protocol will be updated
* (https://github.com/Microsoft/language-server-protocol/issues/3)
*
* @param document
* @param offset
* @return
*/
private IRegion findWord(IDocument document, int offset) {
int start = -2;
int end = -1;
try {
int pos = offset;
char c;
while (pos >= 0 && pos < document.getLength()) {
c = document.getChar(pos);
if (!Character.isUnicodeIdentifierPart(c)) {
break;
}
--pos;
}
start = pos;
pos = offset;
int length = document.getLength();
while (pos < length) {
c = document.getChar(pos);
if (!Character.isUnicodeIdentifierPart(c))
break;
++pos;
}
end = pos;
} catch (BadLocationException x) {
LanguageServerPlugin.logWarning(x.getMessage(), x);
}
if (start >= -1 && end > -1) {
if (start == offset && end == offset)
return new Region(offset, 0);
else if (start == offset)
return new Region(start, end - start);
else
return new Region(start + 1, end - start - 1);
}
return null;
}
}