blob: 863c1a2ef685d85bdcea7031929b82d919891697 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Rogue Wave Software 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:
* Michał Niewrzał (Rogue Wave Software Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e.outline;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.core.resources.IFile;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
public class SymbolsModel {
private static final SymbolInformation ROOT_SYMBOL_INFORMATION = new SymbolInformation();
private static final Object[] EMPTY = new Object[0];
private Map<SymbolInformation, List<SymbolInformation>> childrenMap = new HashMap<>();
private List<DocumentSymbol> rootSymbols = new ArrayList<>();
private Map<DocumentSymbol, DocumentSymbol> parent = new HashMap<>();
private IFile file;
public static class DocumentSymbolWithFile {
public final DocumentSymbol symbol;
public final IFile file;
public DocumentSymbolWithFile(DocumentSymbol symbol, IFile file) {
this.symbol = symbol;
this.file = file;
}
}
public boolean update(List<Either<SymbolInformation, DocumentSymbol>> response) {
// TODO update model only on real change
childrenMap.clear();
rootSymbols.clear();
parent.clear();
if (response != null && !response.isEmpty()) {
Collections.sort(response, Comparator.comparing(
either -> either.isLeft() ? either.getLeft().getLocation().getRange().getStart()
: either.getRight().getRange().getStart(),
// strange need to cast here, could be a JDT compiler issue
Comparator.comparingInt(pos -> ((Position) pos).getLine())
.thenComparingInt(pos -> ((Position) pos).getCharacter())));
Deque<SymbolInformation> parentStack = new ArrayDeque<>();
parentStack.push(ROOT_SYMBOL_INFORMATION);
SymbolInformation previousSymbol = null;
for (Either<SymbolInformation, DocumentSymbol> either : response) {
if (either.isLeft()) {
SymbolInformation symbol = either.getLeft();
if (isIncluded(previousSymbol, symbol)) {
parentStack.push(previousSymbol);
addChild(parentStack.peek(), symbol);
} else if (isIncluded(parentStack.peek(), symbol)) {
addChild(parentStack.peek(), symbol);
} else {
while (!isIncluded(parentStack.peek(), symbol)) {
parentStack.pop();
}
addChild(parentStack.peek(), symbol);
parentStack.push(symbol);
}
previousSymbol = symbol;
} else if (either.isRight()) {
rootSymbols.add(either.getRight());
}
}
}
return true;
}
private boolean isIncluded(SymbolInformation parent, SymbolInformation symbol) {
if (parent == null || symbol == null) {
return false;
}
if (parent == ROOT_SYMBOL_INFORMATION) {
return true;
}
return isIncluded(parent.getLocation(), symbol.getLocation());
}
private boolean isIncluded(Location reference, Location included) {
return reference.getUri().equals(included.getUri())
&& !reference.equals(included)
&& isAfter(reference.getRange().getStart(), included.getRange().getStart())
&& isAfter(included.getRange().getEnd(), reference.getRange().getEnd());
}
private boolean isAfter(Position reference, Position included) {
return included.getLine() > reference.getLine()
|| (included.getLine() == reference.getLine() && included.getCharacter() >= reference.getCharacter());
}
private void addChild(SymbolInformation parent, SymbolInformation child) {
List<SymbolInformation> children = childrenMap.computeIfAbsent(parent, key -> new ArrayList<>());
children.add(child);
}
public Object[] getElements() {
List<Object> res = new ArrayList<>();
res.addAll(Arrays.asList(getChildren(ROOT_SYMBOL_INFORMATION)));
rootSymbols.stream().map(symbol -> new DocumentSymbolWithFile(symbol, this.file)).forEach(res::add);
return res.toArray(new Object[res.size()]);
}
public Object[] getChildren(Object parentElement) {
if (parentElement != null) {
if (parentElement instanceof SymbolInformation) {
List<SymbolInformation> children = childrenMap.get(parentElement);
if (children != null) {
return children.toArray();
}
} else if (parentElement instanceof DocumentSymbolWithFile) {
DocumentSymbolWithFile element = (DocumentSymbolWithFile) parentElement;
return element.symbol.getChildren().stream()
.map(symbol -> new DocumentSymbolWithFile(symbol, element.file)).toArray();
}
}
return EMPTY;
}
public Object getParent(Object element) {
if (element instanceof SymbolInformation) {
Optional<SymbolInformation> result = childrenMap.keySet().stream().filter(parent -> {
List<SymbolInformation> children = childrenMap.get(parent);
return children == null ? false : children.contains(element);
}).findFirst();
return result.isPresent() ? result.get() : null;
} else if (element instanceof DocumentSymbol) {
return parent.get(element);
}
return null;
}
public void setFile(IFile file) {
this.file = file;
}
}