/******************************************************************************* * Copyright (c) 2017 the TeXlipse team 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: * The TeXlipse team - initial API and implementation *******************************************************************************/ package org.eclipse.texlipse.outline; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.texlipse.TexlipsePlugin; import org.eclipse.texlipse.model.MarkerHandler; import org.eclipse.texlipse.model.OutlineNode; import org.eclipse.texlipse.model.TexProjectParser; import org.eclipse.texlipse.properties.TexlipseProperties; /** * Container for an outline representing the entire project * * @author Oskar Ojala */ public class TexProjectOutline { private IProject currentProject; private List<OutlineNode> topLevelNodes; private OutlineNode virtualTopNode; private TexProjectParser fileParser; private Map<String, List<OutlineNode>> outlines = new HashMap<String, List<OutlineNode>>(); private Set<String> included = new HashSet<String>(); /** * Creates a new project outline * * @param currentProject The projet the outline represents * @param labels The labels of the project * @param bibs The bibliography of the project */ public TexProjectOutline(IProject currentProject) { this.currentProject = currentProject; this.fileParser = new TexProjectParser(currentProject); } /** * Adds an outline into the project (full document) outline * * @param nodes The outline tree top * @param fileName The path of the source file relative to the * project's base directory */ public void addOutline(List<OutlineNode> nodes, String fileName) { outlines.put(fileName, nodes); IFile mainFile = TexlipseProperties.getProjectSourceFile(currentProject); String str = mainFile.getFullPath().removeFirstSegments(1).toString(); if (fileName.equals(str)) { this.topLevelNodes = nodes; } } /** * Returns the complete outline starting with the main file * and displaying all the files that are included from the main * file. * * Note that this clears the problem markers from the main file * and each included file. * * @return List containing <code>outlineNode</code>s */ public List<OutlineNode> getFullOutline() { included.clear(); virtualTopNode = new OutlineNode("Entire document", OutlineNode.TYPE_DOCUMENT, 0, null); IFile currentTexFile = TexlipseProperties.getProjectSourceFile(currentProject); MarkerHandler marker = MarkerHandler.getInstance(); marker.clearProblemMarkers(currentTexFile); String fullName = getProjectRelativeName(currentTexFile); if (topLevelNodes == null) { try { topLevelNodes = fileParser.parseFile(currentTexFile); outlines.put(fullName, topLevelNodes); } catch (IOException ioe) { TexlipsePlugin.log("Unable to create full document outline; main file is not parsable", ioe); return new ArrayList<OutlineNode>(); } } included.add(fullName); addChildren(virtualTopNode, topLevelNodes, currentTexFile); List<OutlineNode> outlineTop = virtualTopNode.getChildren(); for (Iterator<OutlineNode> iter = outlineTop.iterator(); iter.hasNext();) { OutlineNode node = iter.next(); node.setParent(null); } return outlineTop; } /** * Replaces an input node with the outline that the referred file contains. * * @param parent The parent node to add the input to * @param insertList The top level nodes of the outline to insert * @param texFile The file that contains the nodes in <code>insertList</code> */ private void replaceInput(OutlineNode parent, List<OutlineNode> insertList, IFile texFile) { // An input node should never have any children // We need to raise the level depending on the type of the 1st node in the new outline if (insertList.size() == 0) { return; } for (Iterator<OutlineNode> iter2 = insertList.iterator(); iter2.hasNext();) { OutlineNode oldNode2 = iter2.next(); if (oldNode2.getType() == OutlineNode.TYPE_INPUT) { // replace node with tree IFile includedFile = resolveFile(oldNode2.getName(), texFile, oldNode2.getBeginLine()); if (includedFile != null) { List<OutlineNode> nodes = loadInput(includedFile, texFile, oldNode2.getBeginLine()); replaceInput(parent, nodes, includedFile); included.remove(getProjectRelativeName(includedFile)); } } else { // TODO do a real comparison method here instead, this doesn't work always while (oldNode2.getType() <= parent.getType()) { parent = parent.getParent(); } OutlineNode newNode = oldNode2.copy(texFile); parent.addChild(newNode); newNode.setParent(parent); List<OutlineNode> oldChildren = oldNode2.getChildren(); if (oldChildren != null) { // TODO do we need to check parent level? addChildren(newNode, oldChildren, texFile); } } } } /** * Adds OutlineNodes in <code>children</code> under the * node <code>main</code> copying the child nodes in the * process. * * @param main The parent node * @param children The child nodes to add to the parent node * @param texFile The file that contains the nodes in <code>insertList</code> */ private boolean addChildren(OutlineNode main, List<OutlineNode> children, IFile texFile) { boolean insert = false; for (Iterator<OutlineNode> iter = children.iterator(); iter.hasNext();) { OutlineNode node = iter.next(); // The tree shape might have changed... if (insert) { OutlineNode newMain = getParentLevel(virtualTopNode.getChildren(), OutlineNode.getSmallerType(node.getType())); main = newMain == null ? virtualTopNode : newMain; } if (node.getType() == OutlineNode.TYPE_INPUT) { // replace node with tree IFile includedFile = resolveFile(node.getName(), texFile, node.getBeginLine()); if (includedFile != null) { List<OutlineNode> nodes = loadInput(includedFile, texFile, node.getBeginLine()); replaceInput(main, nodes, includedFile); included.remove(getProjectRelativeName(includedFile)); insert = true; } } else { OutlineNode newNode = node.copy(texFile); main.addChild(newNode); newNode.setParent(main); List<OutlineNode> oldChildren = node.getChildren(); if (oldChildren != null) { if (addChildren(newNode, oldChildren, texFile)) { main = getParentLevel(virtualTopNode.getChildren(), main.getType()); } } } } return insert; } /** * Gets the appropriate parent node for the given level. * Typically, the children of the top level node are given * as the parameter so that the whole tree is searched. This * method is useful for determining the node to insert new nodes * under after an include ahs taken place. * * @param children The children to start searching from * @param level The level (lower limit) to find * @return Last node of the given level or the highest level that is * lower than the given level */ private OutlineNode getParentLevel(List<OutlineNode> children, int level) { if (children == null || children.size() == 0) { return null; } OutlineNode lastNode = (OutlineNode) children.get(children.size() - 1); if (lastNode.getType() == level) { return lastNode; } else if (lastNode.getType() > level) { return level == OutlineNode.TYPE_DOCUMENT ? lastNode.getParent() : null; } // reverse iteration /* for (ListIterator li = children.listIterator(children.size() - 1); li.hasPrevious();) { List nodeChildren = ((OutlineNode) li.previous()).getChildren(); if (nodeChildren != null) { OutlineNode found = getParentLevel(nodeChildren, level); if (found != null) { return found; } } }*/ List<OutlineNode> nodeChildren = lastNode.getChildren(); if (nodeChildren != null) { OutlineNode found = getParentLevel(nodeChildren, level); if (found != null) { return found; } } // Now return the "best match", i.e. the smallest one that's larger return lastNode; } /** * Resolves from a textual representation the IFile that corresponds * to that file in the current project. * * @param name The name of the file * @param referringFile The file referring to (i.e. including) this file * @param lineNumber The line number of the inclusion command * @return The corresponding IFile or null if no file was found */ private IFile resolveFile(String name, IFile referringFile, int lineNumber) { MarkerHandler marker = MarkerHandler.getInstance(); //Inclusions are always relative to the main file IFile currentTexFile = TexlipseProperties.getProjectSourceFile(currentProject); IFile newTexFile = fileParser.findIFile(name, currentTexFile); if (newTexFile == null) { /* marker.createErrorMarker(referringFile, "Could not find file " + name, lineNumber);*/ return null; } // TODO check that this doesn't get messed up if the same file is included sevral times marker.clearProblemMarkers(newTexFile); return newTexFile; } /** * Loads the outline from the given file. * * @param newTexFile The file to parse * @param referringFile The file referring to (i.e. including) this file * @param lineNumber The line number of the inclusion command * @return The top level nodes of the parsed file or an empty list if parsing * failed */ private List<OutlineNode> loadInput(IFile newTexFile, IFile referringFile, int lineNumber) { MarkerHandler marker = MarkerHandler.getInstance(); String fullName = getProjectRelativeName(newTexFile); List<OutlineNode> nodes = outlines.get(fullName); if (nodes == null) { try { nodes = fileParser.parseFile(newTexFile); outlines.put(fullName, nodes); } catch (IOException ioe) { marker.createErrorMarker(referringFile, "Could not parse file " + fullName + ", reason: " + ioe.getMessage(), lineNumber); return new ArrayList<OutlineNode>(); } } if (!included.add(fullName)) { marker.createErrorMarker(referringFile, "Circular include of " + fullName, lineNumber); return new ArrayList<OutlineNode>(); } return nodes; } /** * Takes an IFile and returns the path to the file and filename * relative to the project root directory. * * @param file The file * @return Path with filename relative to project */ private String getProjectRelativeName(IFile file) { return file.getFullPath().removeFirstSegments(1).toString(); } }