/*******************************************************************************
* Copyright (c) 2007,2008 IBM Corporation.
* 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:
* Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation
*******************************************************************************/
package org.eclipse.imp.services.base;
import java.util.List;
import java.util.Stack;
import lpg.runtime.IPrsStream;
import lpg.runtime.IToken;
import org.eclipse.imp.core.ErrorHandler;
import org.eclipse.imp.parser.IParseController;
import org.eclipse.imp.parser.ISourcePositionLocator;
import org.eclipse.imp.parser.SimpleLPGParseController;
import org.eclipse.imp.services.IOutlineImage;
import org.eclipse.imp.services.IOutliner;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditor;
/**
* OutlinerBase is an abstract base type for a source-text outlining service.
* It is intended to support extensions for language-specific outliners.
* It is adapted from DefaultOutliner by Chris Laffra, but whereas that was
* intended to provide an operational implementation of a generic outliner,
* OutlinerBase explicitly requires completion by language-specific outliner
* implementations. This class is actually abstract only with respect to
* a method that sends a visitor to an AST, as both the visitor and AST types
* are language-specific.
*
* @see org.eclipse.imp.defaults.DefaultOutliner
*
* @author suttons@us.ibm.com
* Updates:
* SMS 21 Jun 2007: Added guards on array indices computed in comparing
* individual tokens
* @deprecated Please consider providing a TreeContentProvider
*/
public abstract class OutlinerBase implements IOutliner
{
protected IParseController fParseController;
public void setEditor(ITextEditor editor) { }
protected Tree tree;
protected class TreeSelectionListener implements SelectionListener
{
public TreeSelectionListener() {
}
public void widgetSelected(SelectionEvent e) {
TreeItem ti= (TreeItem) e.item;
Object data= ti.getData();
// data is assumed to be an instance of an AST node in some
// language-specific type
if (data == null) {
System.out.println("Wiget selected for outline item with no node; returning");
return;
}
// It's actually possible (if not probable) that the outlining service will be invoked
// by the editor so soon in the initialization of a workspace that the following composite
// call will fail (e.g., due to null values returned at various points). However, execution
// of this method depends on the selection of a widget, which requires that the ActivePage,
// ActiveWorkbenchWindow, etc. be established. So for the outliner, this sequence of calls
// is not a problem in practice.
IEditorPart activeEditor= PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
ITextEditor textEditor= (ITextEditor) activeEditor;
ISourcePositionLocator nodeLocator = fParseController.getSourcePositionLocator();
int startOffset = 0;
int endOffset = 0;
try {
// The methods of nodeLocator typically take an object and cast it
// to an ASTNode type without any concern for what it actually is,
// so try that here and catch any exception that is thrown
startOffset = nodeLocator.getStartOffset(data);
endOffset = nodeLocator.getEndOffset(data);
} catch (ClassCastException x) {
System.err.println("OutlinerBase.TreeSelectionListener.wigetSelected: " +
"ClassCastException trying to treat event data as AST node type");
return;
}
textEditor.selectAndReveal(startOffset, endOffset-startOffset+1);
}
public void widgetDefaultSelected(SelectionEvent e) { }
}
public void setTree(Tree tree) {
this.tree = tree;
this.tree.addSelectionListener(new TreeSelectionListener());
}
/*
* Fields and methods for representing and manipulating
* the stack of outline items
*/
private Stack<TreeItem> fItemStack = new Stack<TreeItem>();
public void pushTopItem(String itemName, Object node) {
fItemStack.push(createTopItem(itemName, node));
}
public void pushTopItem(String itemName, Object node, IOutlineImage image) {
fItemStack.push(createTopItem(itemName, node, image));
}
public void pushTopItem(String itemName, Object node, IOutlineImage image, int style) {
fItemStack.push(createTopItem(itemName, node, image, style));
}
public void pushSubItem(String itemName, Object node) {
fItemStack.push(createSubItem(itemName, node));
}
public void pushSubItem(String itemName, Object node, IOutlineImage image) {
fItemStack.push(createSubItem(itemName, node, image));
}
public void pushSubItem(String itemName, Object node, IOutlineImage image, int style) {
fItemStack.push(createSubItem(itemName, node, image, style));
}
public void popSubItem() {
fItemStack.pop();
}
public void addSubItem(String label, Object node) {
createSubItem(label, node);
}
public void addSubItem(String label, Object node, IOutlineImage image) {
createSubItem(label, node, image);
}
public void addSubItem(String label, Object node, IOutlineImage image, int style) {
createSubItem(label, node, image, style);
}
/*
* Following are two pairs of methods, one for creating new top-level
* items, and one for creating new sub items, in the outline tree.
* The first method of each pair has been hoisted from the outline
* template and modified to reference a default outline image (to break
* the dependence on a language-specific image). The second method of
* each pair is analogous to the first but takes an IOutlineImage
* to allow language-specific images to be provided.
*
* (Note: IOutlineImage and DefaultOutlineImage were newly created
* to enable this refactoring.)
*/
public TreeItem createTopItem(String label, Object n) {
TreeItem treeItem= new TreeItem(tree, SWT.NONE);
treeItem.setText(label);
treeItem.setImage(DefaultOutlineImage.getDefaultOutlineImage().getOutlineItemImage());
if (n != null)
treeItem.setData(n);
return treeItem;
}
public TreeItem createTopItem(String label, Object n, IOutlineImage image) {
TreeItem treeItem= new TreeItem(tree, SWT.NONE);
treeItem.setText(label);
treeItem.setImage(image.getOutlineItemImage());
if (n != null)
treeItem.setData(n);
return treeItem;
}
public TreeItem createTopItem(String label, Object n, IOutlineImage image, int style) {
TreeItem treeItem= new TreeItem(tree, style);
treeItem.setText(label);
treeItem.setImage(image.getOutlineItemImage());
if (n != null)
treeItem.setData(n);
return treeItem;
}
public TreeItem createSubItem(String label, Object n) {
TreeItem treeItem= new TreeItem((TreeItem) fItemStack.peek(), SWT.NONE);
treeItem.setText(label);
if (n != null)
treeItem.setData(n);
treeItem.setImage(DefaultOutlineImage.getDefaultOutlineImage().getOutlineItemImage());
return treeItem;
}
public TreeItem createSubItem(String label, Object n, IOutlineImage image) {
TreeItem treeItem= new TreeItem((TreeItem) fItemStack.peek(), SWT.NONE);
treeItem.setText(label);
if (n != null)
treeItem.setData(n);
treeItem.setImage(image.getOutlineItemImage());
return treeItem;
}
public TreeItem createSubItem(String label, Object n, IOutlineImage image, int style) {
TreeItem treeItem= new TreeItem((TreeItem) fItemStack.peek(), style);
treeItem.setText(label);
if (n != null)
treeItem.setData(n);
treeItem.setImage(image.getOutlineItemImage());
// int itemStyle = treeItem.getStyle();
return treeItem;
}
/**
* Create the outline tree representing the AST associated with a given
* parse controller. There are two issues of particular concern in the
* design of this method: observing the appropriate UI protocol for
* redrawing the tree, and filtering out unwanted invocations that
* reflect events that should probably not trigger a redrawing of the tree.
*
* Regarding the protocol for redrawing the tree, it seems that calls to
* tree.setRedraw(true) must be balanced by calls to tree.setRedraw(false).
* This is because of logic in org.eclipse.swt.widgets.Control, the parent
* class of org.eclipse.swt.widgets.TreeItem. If there are excessive calls
* to tree.setRedraw(true) then the outline view can become "stuck" on a
* particular file; that is, when a new editor is opened or brought to
* the foreground, the outline view continues to show the outline for a
* previous file. The implementation provided here generally assures,
* insofar as possible, that the calls to tree.setRedraw(true) and to
* tree.setRedraw(false) are balanced, even in the face of exceptions.
* If the logic of this method is modified, care should be taken to assure
* that this balance is maintained.
*
* Regarding unwanted invocations, this method may be called even when
* the AST represented has not changed and thus when redrawing of
* the outline may be unnecessary. Unnecessary redrawing of the outline
* is probably a negligable inefficiency in most cases, but redrawing has
* the effect of collapsing an expanded outline, which can be annoying
* for users. Folding of the source text in an editor is one case in which
* this method is called when there is no change to the underlying AST.
* The implementation provided here includes a test for "significantChange"
* and avoids redrawing the outline when no significant change has occurred.
* The provided version of significantChange(..) tracks changes to the
* whole tree, the number of tokens, and the text associated with individual
* tokens. Users who want to use an alternative definition of "significant
* change" should reimplement the change-detection strategy type.
*
* @param controller A parse controller from which an AST can be obtained
* to serve as the basis of an outline presentation
*
* @param offset Currently ignored; kept for backward compatibility
*
* @return void Has no return but has the effect of (re)drawing
* an outline tree based on the AST obtained from the
* given parse controller, if possible.
*
* Has no effect under a variety of problematic
* conditions: given parse controller is null, outline
* tree is null, parse controller has errors, or parse
* controller's current AST is null. Also has no
* effect if there is no "significant change" from the
* the AST on the previous invocation to the AST on this
* invocation.
*
* @exception Throws none; traps all exceptions and reports an
* error.
*/
public void createOutlinePresentation(IParseController controller, int offset)
{
if (controller == null || tree == null) return;
if (!significantChange(controller)) return;
fParseController= controller;
boolean redrawSetFalse = false;
try {
Object root= controller.getCurrentAst();
if (root == null) return;
// SMS 4 Dec 2007: added guard
if (!tree.isDisposed()) {
tree.setRedraw(false);
redrawSetFalse = true;
tree.removeAll();
}
fItemStack.clear();
sendVisitorToAST(root);
} catch (Throwable e) {
ErrorHandler.reportError("Exception generating outlinel", e);
} finally {
if (redrawSetFalse) {
tree.setRedraw(true);
}
}
}
/*
* To hold in a generic way any data that the method "significantChange(..)"
* may require from one invocation to the next.
*/
private Object[] previous = null;
/**
* This method is intended to detect whether there has been a significant
* change in the AST represented by the given controller (presumably such
* that the outline should be redrawn).
*
* The default implementation returns true if and only if the parse controller
* has changed (implying a different parse stream and token stream in any case)
* or, for an unchanged parse controller, if there has been a change in the
* number, size, or content of tokens in the current token stream. Note that,
* by this definition, changes in white space between tokens are not considered
* significant.
*
* This method should be overridden in subclasses to implement
* language-specific tests.
*
* @param controller Provides access to the AST represented in the outline
* @return Whether there has been a significant change in the
* AST (by default TRUE)
*/
protected boolean significantChange(IParseController controller) {
// TODO: Override this in the language-specific outliner,
// adopting a language-specific change-detection strategy as
// appropriate
//return true;
boolean previousWasNull = previous == null;
boolean result = false;
// Check for previous values being null (as in uninitialized)
if (previousWasNull) {
// create and initialize previous
previous = new Object[3];
for (int i = 0; i < previous.length; i++) {
previous[i] = null;
}
// check for current and previous controllers both null
if (controller == null) {
return false;
}
}
// If here then had some previous values (although these
// could individually be null); is current controller null?
if (controller == null) {
for (int i = 0; i < previous.length; i++) {
if (previous[i] == null) continue; // not changed
result = true; // changed
previous[i] = null; // null now
}
return result;
}
// If here then had some previous values and have some current
// values; these need to be compared
// (for simplicity assume that current values are not null)
// Get current values for comparison to previous
final IPrsStream parseStream= ((SimpleLPGParseController) controller).getParser().getIPrsStream();
List tokens = parseStream.getTokens();
char[] chars = parseStream.getInputChars();
// Get previous values for comparison to current
IParseController previousController = (IParseController) previous[0];
List previousTokens = (List) previous[1];
char[] previousChars = (char[]) previous[2];
// Update previous values to current values in any case (now that
// we've saved previous in local fields)
previous[0] = controller;
previous[1] = tokens;
previous[2] = chars;
// Compare current and previous values; return true if different
// Are the whole trees different? (Assume so if controllers differ)
if (previousController != controller) return true;
// Are the sizes of the trees different?
if (previousTokens.size() != tokens.size()) {
return true;
}
// Are any of the individual tokens different?
for (int i = 0; i < previousTokens.size()-1; i++) {
IToken previousToken = (IToken)previousTokens.get(i);
IToken token = (IToken)tokens.get(i);
if (previousToken.getKind() != token.getKind()) {
//System.out.println("Previous and current tokens differ at token # = " + i);
return true;
}
int previousStart = previousToken.getStartOffset();
int previousEnd = previousToken.getEndOffset();
int start = token.getStartOffset();
int end = token.getEndOffset();
if ((previousEnd - previousStart) != (end - start)) {
// System.out.println("Previous and current tokens have different extents at token # = " + i);
return true;
}
for (int j = 0; j < (previousEnd - previousStart + 1); j++) {
if (previousStart+j < previousChars.length && start+j < chars.length) {
if ((previousChars.length != 0) && previousChars[previousStart+j] != chars[start+j]) {
// System.out.println("Previous and current tokens have different characters at token # = " + i +
// ", character # = " + j);
return true;
}
}
}
}
// No significant differences found
return false;
}
/**
* Intended to treat the given Object as the root of an AST to which an
* outline visitor should be sent. Both the AST node and the implicit visitor
* are presumed to be language-specific types that will be known in a subclass
* of this class.
*
* @param node Presumably the root of an AST of some language-specific
* type
*/
protected abstract void sendVisitorToAST(Object node);
}