de.codesourcery.jasm16.ide.ui.views.SourceCodeView.java Source code

Java tutorial

Introduction

Here is the source code for de.codesourcery.jasm16.ide.ui.views.SourceCodeView.java

Source

package de.codesourcery.jasm16.ide.ui.views;

/**
 * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentEvent.EventType;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AbstractDocument.DefaultDocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.DefaultStyledDocument.AttributeUndoableEdit;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.View;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import de.codesourcery.jasm16.ast.AST;
import de.codesourcery.jasm16.ast.ASTNode;
import de.codesourcery.jasm16.ast.CommentNode;
import de.codesourcery.jasm16.ast.EndMacroNode;
import de.codesourcery.jasm16.ast.IPreprocessorDirective;
import de.codesourcery.jasm16.ast.IncludeSourceFileNode;
import de.codesourcery.jasm16.ast.InstructionNode;
import de.codesourcery.jasm16.ast.InvokeMacroNode;
import de.codesourcery.jasm16.ast.LabelNode;
import de.codesourcery.jasm16.ast.RegisterReferenceNode;
import de.codesourcery.jasm16.ast.StartMacroNode;
import de.codesourcery.jasm16.ast.StatementNode;
import de.codesourcery.jasm16.ast.SymbolReferenceNode;
import de.codesourcery.jasm16.compiler.CompilationListener;
import de.codesourcery.jasm16.compiler.ICompilationError;
import de.codesourcery.jasm16.compiler.ICompilationUnit;
import de.codesourcery.jasm16.compiler.ISymbol;
import de.codesourcery.jasm16.compiler.ISymbolTable;
import de.codesourcery.jasm16.compiler.Severity;
import de.codesourcery.jasm16.compiler.SourceLocation;
import de.codesourcery.jasm16.compiler.io.AbstractResourceResolver;
import de.codesourcery.jasm16.compiler.io.DefaultResourceMatcher;
import de.codesourcery.jasm16.compiler.io.FileResource;
import de.codesourcery.jasm16.compiler.io.FileResourceResolver;
import de.codesourcery.jasm16.compiler.io.IResource;
import de.codesourcery.jasm16.compiler.io.IResource.ResourceType;
import de.codesourcery.jasm16.compiler.io.IResourceResolver;
import de.codesourcery.jasm16.compiler.phases.ExpandMacrosPhase;
import de.codesourcery.jasm16.exceptions.ResourceNotFoundException;
import de.codesourcery.jasm16.ide.IAssemblyProject;
import de.codesourcery.jasm16.ide.IWorkspace;
import de.codesourcery.jasm16.ide.IWorkspaceListener;
import de.codesourcery.jasm16.ide.NavigationHistory;
import de.codesourcery.jasm16.ide.NavigationHistory.INavigationHistoryListener;
import de.codesourcery.jasm16.ide.NavigationHistory.Location;
import de.codesourcery.jasm16.ide.WorkspaceListener;
import de.codesourcery.jasm16.ide.ui.utils.UIUtils;
import de.codesourcery.jasm16.ide.ui.viewcontainers.EditorContainer;
import de.codesourcery.jasm16.utils.ITextRegion;
import de.codesourcery.jasm16.utils.Line;
import de.codesourcery.jasm16.utils.Misc;
import de.codesourcery.jasm16.utils.TextRegion;

/**
 * Abstract base-class for views that display source code.
 * 
 * <p>This class provides common functionality like
 * syntax-highlighing , navigation history and so forth.</p>
 * 
 * @author tobias.gierke@code-sourcery.de
 */
public abstract class SourceCodeView extends AbstractView implements IEditorView {

    private static final Logger LOG = Logger.getLogger(SourceCodeView.class);

    // time to wait before recompiling after the user changed the source code
    private static final int RECOMPILATION_DELAY_MILLIS = 300;

    // UI widgets
    private volatile JPanel panel;

    private Object currentHighlight;

    private boolean registeredWithTooltipManager = false;
    private volatile boolean editable;
    private SearchDialog searchDialog;

    private Object currentUnderlineHighlight;

    private final NavigationHistory navigationHistory;

    private final PopupListener popupListener = new PopupListener();
    private final UndoManager undoManager = new UndoManager();
    private final UndoableEditListener undoListener = new UndoableEditListener() {
        public void undoableEditHappened(UndoableEditEvent e) {
            UndoableEdit edit = e.getEdit();
            if (edit instanceof AttributeUndoableEdit) {
                return;
            } else if (edit instanceof DefaultDocumentEvent) {
                if (((DefaultDocumentEvent) edit).getType() == EventType.CHANGE) {
                    return;
                }
            }
            undoManager.addEdit(e.getEdit());
            undoAction.updateUndoState();
            redoAction.updateRedoState();
        }
    };

    protected abstract class UndoRedoAction extends AbstractAction {

        public void updateUndoState() {
            if (undoManager.canUndo()) {
                setEnabled(true);
                putValue(Action.NAME, undoManager.getUndoPresentationName());
            } else {
                setEnabled(false);
                putValue(Action.NAME, "Undo");
            }
        }

        public void updateRedoState() {
            if (undoManager.canRedo()) {
                setEnabled(true);
                putValue(Action.NAME, undoManager.getRedoPresentationName());
            } else {
                setEnabled(false);
                putValue(Action.NAME, "Redo");
            }
        }
    }

    private final UndoRedoAction undoAction = new UndoRedoAction() {

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                undoManager.undo();
            } catch (CannotUndoException ex) {
                LOG.error("Unable to undo: " + ex, ex);
            }
            updateUndoState();
            redoAction.updateRedoState();
        }
    };

    private final UndoRedoAction redoAction = new UndoRedoAction() {

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                undoManager.redo();
            } catch (CannotRedoException ex) {
                LOG.error("Unable to redo: " + ex, ex);
            }
            updateRedoState();
            undoAction.updateUndoState();
        }

    };

    private final JTextField cursorPosition = new JTextField();
    private final JTextPane editorPane = new JTextPane();

    private volatile int documentListenerDisableCount = 0;
    private JScrollPane editorScrollPane;

    private final SimpleAttributeSet registerStyle;
    private final SimpleAttributeSet commentStyle;
    private final SimpleAttributeSet instructionStyle;
    private final SimpleAttributeSet labelStyle;
    private final SimpleAttributeSet preProcessorStyle;

    private final SimpleAttributeSet errorStyle;
    private final SimpleAttributeSet defaultStyle;

    private final SimpleAttributeSet macroDefinitionStyle;
    private final SimpleAttributeSet macroInvocationStyle;

    // compiler
    private final IResourceResolver resourceResolver;
    protected final IWorkspace workspace;

    private final INavigationHistoryListener navigationHistoryListener = new INavigationHistoryListener() {

        @Override
        public void navigationHistoryChanged() {
            UIUtils.invokeLater(new Runnable() {

                @Override
                public void run() {
                    SourceCodeView.this.onNavigationHistoryChange();
                }

            });
        }
    };
    private volatile boolean navigationHistoryUpdatesEnabled = true; // controls whether the CaretListener will forward caret position changes to the NavigationHistory
    private volatile boolean isBuilding = false;

    private final IWorkspaceListener workspaceListener = new WorkspaceListener() {

        public void projectDeleted(IAssemblyProject deletedProject) {
            if (deletedProject.isSame(project)) {
                dispose();
            }
        }

        public void buildStarted(IAssemblyProject project) {
            if (project.isSame(getCurrentProject())) {
                isBuilding = true;
            }
        }

        public void buildFinished(IAssemblyProject project, boolean success) {
            if (project.isSame(getCurrentProject())) {
                isBuilding = false;
            }
        };

        public void projectClosed(IAssemblyProject closedProject) {
            if (closedProject.isSame(project)) {
                dispose();
            }
        }

        private void dispose() {
            if (getViewContainer() != null) {
                getViewContainer().disposeView(SourceCodeView.this);
            } else {
                SourceCodeView.this.dispose();
            }
        }

        public void resourceDeleted(IAssemblyProject project, IResource deletedResource) {
            if (DefaultResourceMatcher.INSTANCE.isSame(persistentResource, deletedResource)) {
                dispose();
            }
        }
    };

    private IAssemblyProject project;
    private String initialHashCode; // hash code used to check whether current editor content differs from the one on disk
    private IResource persistentResource; // source code on disk
    private InMemorySourceResource sourceInMemory; // possibly edited source code (in RAM / JEditorPane)
    private ICompilationUnit compilationUnit;

    private CompilationThread compilationThread = null;

    protected static final class UnderlineHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter {

        private int thickness;

        public UnderlineHighlightPainter(Color c, int thickness) {
            super(c);
            this.thickness = thickness;
        }

        @Override
        public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view) {
            Rectangle r;
            if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) {
                // Contained in view, can just use bounds.
                if (bounds instanceof Rectangle) {
                    r = (Rectangle) bounds;
                } else {
                    r = bounds.getBounds();
                }
            } else {
                // Should only render part of View.
                try {
                    // --- determine locations ---
                    Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward,
                            bounds);
                    r = (shape instanceof Rectangle) ? (Rectangle) shape : shape.getBounds();
                } catch (BadLocationException e) {
                    // can't render
                    r = null;
                }
            }

            if (r != null) {
                Color color = getColor();

                if (color == null) {
                    color = c.getSelectionColor();
                }
                g.setColor(color);

                // If we are asked to highlight, we should draw something even
                // if the model-to-view projection is of zero width (6340106).
                r.width = Math.max(r.width, 1);
                g.fillRect(r.x, r.y + r.height, r.width, thickness);
            }

            return r;
        }
    }

    /*  WAIT_FOR_EDIT-------> WAIT_FOR_TIMEOUT ------>( do compilation ) ----+
     *     ^    ^                     |                                      |
     *     |    |                     |                                      |
     *     |    +---RESTART_TIMEOUT---+                                      |
     *     +-----------------------------------------------------------------+
     */
    private enum WaitState {
        WAIT_FOR_EDIT, WAIT_FOR_TIMEOUT, RESTART_TIMEOUT;
    }

    protected static final class StatusMessage {
        private final Severity severity;
        private final ITextRegion location;
        private final String message;
        @SuppressWarnings("unused")
        private final Throwable cause;
        private final ICompilationError error;

        public StatusMessage(Severity severity, String message) {
            this(severity, null, message, null, null);
        }

        public StatusMessage(Severity severity, ITextRegion location, String message) {
            this(severity, location, message, null, null);
        }

        public StatusMessage(Severity severity, ICompilationError error) {
            this(severity, error.getLocation(), error.getMessage(), error, error.getCause());
        }

        public StatusMessage(Severity severity, ITextRegion location, String message, ICompilationError error,
                Throwable cause) {
            if (severity == null) {
                throw new IllegalArgumentException("severity must not be NULL.");
            }

            if (StringUtils.isBlank(message)) {
                throw new IllegalArgumentException("message must not be NULL/blank.");
            }

            this.severity = severity;
            this.location = location;
            this.message = message;
            if (cause == null) {
                this.cause = error != null ? error.getCause() : null;
            } else {
                this.cause = cause;
            }
            this.error = error;
        }

        public StatusMessage(Severity severity, String message, Throwable e) {
            this(severity, null, message, null, e);
        }

        public Severity getSeverity() {
            return severity;
        }

        public ITextRegion getLocation() {
            return location;
        }

        public String getMessage() {
            return message;
        }

        public ICompilationError getError() {
            return error;
        }
    }

    protected final Highlighter getHighlighter() {
        return editorPane.getHighlighter();
    }

    protected class StatusModel extends AbstractTableModel {
        private final List<StatusMessage> messages = new ArrayList<StatusMessage>();

        private final int COL_SEVERITY = 0;
        private final int COL_LOCATION = 1;
        private final int COL_MESSAGE = 2;

        public StatusModel() {
            super();
        }

        @Override
        public int getRowCount() {
            return messages.size();
        }

        public StatusMessage getMessage(int row) {
            return messages.get(row);
        }

        public void addMessage(StatusMessage msg) {
            if (msg == null) {
                throw new IllegalArgumentException("msg must not be NULL.");
            }
            int index = messages.size();
            messages.add(msg);
            fireTableRowsInserted(index, index);
        }

        public void setMessage(StatusMessage msg) {
            if (msg == null) {
                throw new IllegalArgumentException("msg must not be NULL.");
            }
            messages.clear();
            messages.add(msg);
            fireTableDataChanged();
        }

        @Override
        public int getColumnCount() {
            return 3;
        }

        @Override
        public String getColumnName(int columnIndex) {
            switch (columnIndex) {
            case COL_SEVERITY:
                return "Severity";
            case COL_LOCATION:
                return "Location";
            case COL_MESSAGE:
                return "Message";
            default:
                return "no column name?";
            }
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return String.class;
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return false;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            final StatusMessage msg = messages.get(rowIndex);
            switch (columnIndex) {
            case COL_SEVERITY:
                return msg.getSeverity().toString();
            case COL_LOCATION:
                if (msg.getLocation() != null) {
                    SourceLocation location;
                    try {
                        location = getSourceLocation(msg.getLocation());
                        return "Line " + location.getLineNumber() + " , column " + location.getColumnNumber();
                    } catch (NoSuchElementException e) {
                        // ok, can't help it
                    }
                }
                return "<unknown>";
            case COL_MESSAGE:
                return msg.getMessage();
            default:
                return "no column name?";
            }
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            throw new UnsupportedOperationException("");
        }

        public void addError(String message, IOException e1) {
            addMessage(new StatusMessage(Severity.ERROR, message, e1));
        }

        public void addInfo(String message) {
            addMessage(new StatusMessage(Severity.INFO, message));
        }

        public void clearMessages() {
            messages.clear();
            fireTableDataChanged();
        }

    }

    protected final void replaceText(ITextRegion region, String newValue) {
        disableDocumentListener();
        try {
            editorPane.getDocument().remove(region.getStartingOffset(), region.getLength());
            editorPane.getDocument().insertString(region.getStartingOffset(), newValue, defaultStyle);
        } catch (BadLocationException e) {
            throw new RuntimeException(e);
        } finally {
            enableDocumentListener();
        }
    }

    protected class CompilationThread extends Thread {

        private final Object LOCK = new Object();

        // @GuardedBy( LOCK )
        private WaitState currentState = WaitState.WAIT_FOR_EDIT;

        public CompilationThread() {
            setDaemon(true);
        }

        @Override
        public void run() {
            while (true) {
                try {
                    internalRun();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        private void internalRun() throws InterruptedException, InvocationTargetException {
            synchronized (LOCK) {
                switch (currentState) {
                case WAIT_FOR_EDIT:
                    LOCK.wait();
                    return;
                case RESTART_TIMEOUT:
                    currentState = WaitState.WAIT_FOR_TIMEOUT; // $FALL-THROUGH$
                    return;
                case WAIT_FOR_TIMEOUT:
                    LOCK.wait(RECOMPILATION_DELAY_MILLIS);
                    if (currentState != WaitState.WAIT_FOR_TIMEOUT) {
                        return;
                    }
                }
            }

            try {
                SwingUtilities.invokeAndWait(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            validateSourceCode();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {

                        }
                    }
                });
            } finally {
                synchronized (LOCK) {
                    currentState = WaitState.WAIT_FOR_EDIT;
                }
            }
        }

        public void documentChanged() {
            synchronized (LOCK) {
                currentState = WaitState.RESTART_TIMEOUT;
                LOCK.notifyAll();
            }
        }
    }

    protected final void notifyDocumentChanged() {
        updateTitle();

        if (compilationThread == null) {
            compilationThread = new CompilationThread();
            compilationThread.start();
        }
        compilationThread.documentChanged();
    }

    private final DocumentFilter documentFilter = new DocumentFilter() {

        private Line getLine(int offset) {
            try {
                return compilationUnit.getLineForOffset(offset);
            } catch (NoSuchElementException e) {
                return null;
            }
        }

        private int getIndentionOfPreviousLine(int currentOffset) {
            if (compilationUnit == null || compilationUnit.getAST() == null) {
                return -1;
            }

            Line previous = null;
            try {
                previous = compilationUnit.getLineForOffset(currentOffset);
            } catch (NoSuchElementException e) {
                return -1;
            }

            while (previous != null) {
                StatementNode stmt = compilationUnit.getAST()
                        .getFirstStatementForOffset(previous.getLineStartingOffset());
                if (stmt != null && stmt.hasChildren()) {
                    return stmt.child(0).getTextRegion().getStartingOffset() - previous.getLineStartingOffset();
                }
                previous = compilationUnit.getPreviousLine(previous);
            }
            return -1;
        }

        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
                throws BadLocationException {
            if (text.equals("\n") && length == 0) {
                final int indention = getIndentionOfPreviousLine(offset);
                if (indention > 0) {
                    super.replace(fb, offset, length, text + StringUtils.repeat(" ", indention), attrs);
                    return;
                }
            }
            super.replace(fb, offset, length, text, attrs);
        }
    };

    private final DocumentListener recompilationListener = new DocumentListener() {

        private void textChanged(DocumentEvent e) {
            notifyDocumentChanged();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            textChanged(e);
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            textChanged(e);
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            /* do nothing, style change only */ }
    };

    private final CaretListener listener = new CaretListener() {

        @Override
        public void caretUpdate(final CaretEvent e) {
            // according to JDK docs, caretUpdate() is NOT necessarily called by the EDT,
            // wrap all code so we can safely update UI components 
            final Runnable r = new Runnable() {

                public void run() {
                    // gotoLocation() will set updateNavigationHistory == false when it's been
                    // programatically triggered
                    if (navigationHistoryUpdatesEnabled && sourceInMemory != null) {
                        navigationHistory.add(new Location(project, sourceInMemory, e.getDot()));
                    }

                    // do not fire caret updates while building, this 
                    // causes at least the SourceEditorView to access the AST that
                    // is still under construction and trigger a NPE in ASTNode#getNodeInRange()                
                    if (!isEditable() || isBuilding()) {
                        return;
                    }

                    if (compilationUnit != null && compilationUnit.getAST() != null
                            && compilationUnit.getAST().getTextRegion() != null) {
                        try {
                            final SourceLocation location = getSourceLocation(e.getDot());
                            cursorPosition.setHorizontalAlignment(JTextField.RIGHT);
                            cursorPosition.setText("Line " + location.getLineNumber() + " , column "
                                    + location.getColumnNumber() + " (offset " + e.getDot() + ")");
                        } catch (NoSuchElementException e2) {
                            // ok, user clicked on unknown location
                        }
                    }
                    onCaretUpdate(e);
                };
            };
            UIUtils.invokeLater(r);
        }
    };

    protected void onCaretUpdate(CaretEvent e) {

    }

    public SourceCodeView(IResourceResolver resourceResolver, IWorkspace workspace,
            NavigationHistory navigationHistory, boolean isEditable) {
        if (workspace == null) {
            throw new IllegalArgumentException("workspace must not be null");
        }
        if (resourceResolver == null) {
            throw new IllegalArgumentException("resourceResolver must not be NULL.");
        }
        this.navigationHistory = navigationHistory;
        this.resourceResolver = resourceResolver;
        this.editable = isEditable;
        this.workspace = workspace;
        defaultStyle = new SimpleAttributeSet();
        errorStyle = createStyle(Color.RED);
        registerStyle = createStyle(Color.ORANGE);
        commentStyle = createStyle(Color.WHITE);
        macroDefinitionStyle = createStyle(Color.YELLOW);
        macroInvocationStyle = createStyle(Color.YELLOW);
        instructionStyle = createStyle(new Color(50, 186, 223));
        labelStyle = createStyle(new Color(237, 237, 81));
        preProcessorStyle = createStyle(new Color(200, 200, 200));
        workspace.addWorkspaceListener(workspaceListener);
        navigationHistory.addListener(navigationHistoryListener);
    }

    protected final static SimpleAttributeSet createStyle(Color color) {
        SimpleAttributeSet result = new SimpleAttributeSet();
        StyleConstants.setForeground(result, color);
        return result;
    }

    protected final SourceLocation getSourceLocation(ITextRegion range) {
        return getSourceLocation(range.getStartingOffset());
    }

    protected final SourceLocation getSourceLocation(int offset) {
        final Line line = compilationUnit.getLineForOffset(offset);
        return new SourceLocation(compilationUnit, line, new TextRegion(offset, 0));
    }

    protected final GridBagConstraints constraints(int x, int y, int fill) {
        GridBagConstraints result = new GridBagConstraints();
        result.fill = fill;
        result.weightx = 1.0;
        result.weighty = 1.0;
        result.gridheight = 1;
        result.gridwidth = 1;
        result.gridx = x;
        result.gridy = y;
        result.insets = new Insets(1, 1, 1, 1);
        return result;
    }

    protected void setStatusMessage(String message) {

    }

    protected final String getTextFromTextPane() {
        final int len = editorPane.getDocument().getLength();
        if (len == 0) {
            return "";
        }
        try {
            return editorPane.getDocument().getText(0, len);
        } catch (BadLocationException e) {
            throw new RuntimeException("bad location: ", e);
        }
    }

    @Override
    public final void openResource(IAssemblyProject project, IResource resource, int caretPosition)
            throws IOException {
        if (this.project != project || this.persistentResource != resource) {
            openResource(project, resource, caretPosition, true);
        }
    }

    protected final void openResource(final IAssemblyProject project, final IResource sourceFile,
            boolean compileSource) throws IOException {
        openResource(project, sourceFile, 0, compileSource);
    }

    protected final void openResource(final IAssemblyProject project, final IResource sourceFile, int caretPosition,
            boolean compileSource) throws IOException {
        if (project == null) {
            throw new IllegalArgumentException("project must not be NULL");
        }
        if (sourceFile == null) {
            throw new IllegalArgumentException("sourceFile must not be NULL");
        }

        // read source first so we don't discard internal state
        // and end up with an IOException later on...
        final String source = Misc.readSource(sourceFile);
        this.initialHashCode = Misc.calcHash(source);

        this.project = project;
        if (sourceFile instanceof InMemorySourceResource) {
            this.sourceInMemory = (InMemorySourceResource) sourceFile;
            this.persistentResource = sourceInMemory.getPersistentResource();
        } else {
            this.sourceInMemory = new InMemorySourceResource(sourceFile, editorPane) {

                @Override
                public String toString() {
                    return "SourceCodeView[ " + persistentResource + " ]";
                }
            };
            this.persistentResource = sourceFile;
        }
        clearHighlight();

        try {
            disableDocumentListener();
            try {
                final Document doc = editorPane.getDocument();
                doc.putProperty(Document.StreamDescriptionProperty, null);

                disableNavigationHistoryUpdates();
                System.out.println("Text length: " + (source == null ? 0 : source.length()));

                try {
                    editorPane.setText(source);
                } finally {
                    enableNavigationHistoryUpdates();
                }

                try {
                    editorPane.setCaretPosition(caretPosition);
                } catch (IllegalArgumentException e) {
                    LOG.error("openResource(): Invalid caret position " + caretPosition + " in resource "
                            + sourceFile);
                }

                if (panel != null) {
                    ICompilationUnit existing = null;
                    if (!compileSource) {
                        existing = project.getProjectBuilder().getCompilationUnit(sourceFile);
                    }
                    validateSourceCode(existing);
                }
            } finally {
                enableDocumentListener();
            }
        } finally {
            enableNavigationHistoryUpdates();
        }

        editorPane.requestFocus();
        updateTitle();
    }

    protected final boolean isBuilding() {
        return isBuilding;
    }

    protected final void validateSourceCode() throws IOException {
        validateSourceCode(null);
    }

    protected final void validateSourceCode(ICompilationUnit existing) throws IOException {

        long time = -System.currentTimeMillis();
        disableDocumentListener();
        try {
            clearCompilationErrors();

            onSourceCodeValidation();

            final IResourceResolver delegatingResolver = new AbstractResourceResolver() {

                private IResourceResolver getChildResourceResolver(IResource parent) {
                    IResource r = parent == null ? getCurrentResource() : parent;
                    if (!(r instanceof FileResource)) {
                        if (r instanceof InMemorySourceResource) {
                            r = ((InMemorySourceResource) r).getPersistentResource();
                        }
                    }
                    if (!(r instanceof FileResource)) {
                        throw new RuntimeException("Internal error, not a file-resource: " + getCurrentResource());
                    }
                    final FileResource fr = (FileResource) r;
                    return new FileResourceResolver(fr.getAbsoluteFile().getParentFile()) {
                        @Override
                        protected ResourceType determineResourceType(File file) {
                            // TODO: Maybe implement some more general mechanism of determining resource types ?
                            return project.getConfiguration().isSourceFile(file) ? ResourceType.SOURCE_CODE
                                    : ResourceType.UNKNOWN;
                        }
                    };
                }

                @Override
                public IResource resolve(String identifier) throws ResourceNotFoundException {
                    try {
                        if (resourceResolver != null) {
                            return resourceResolver.resolve(identifier);
                        }
                    } catch (ResourceNotFoundException e) {
                    }
                    return getChildResourceResolver(null).resolve(identifier);
                }

                @Override
                public IResource resolveRelative(String identifier, IResource parent)
                        throws ResourceNotFoundException {
                    try {
                        if (resourceResolver != null) {
                            return resourceResolver.resolveRelative(identifier, parent);
                        }
                    } catch (ResourceNotFoundException e) {
                    }

                    final IResource realParent;
                    if (parent instanceof InMemorySourceResource) {
                        realParent = ((InMemorySourceResource) parent).getPersistentResource();
                    } else {
                        realParent = parent;
                    }
                    return getChildResourceResolver(parent).resolveRelative(identifier, realParent);
                }
            };

            try {
                if (existing == null || existing.getAST() == null) {
                    compilationUnit = project.getProjectBuilder().parse(sourceInMemory, delegatingResolver,
                            new CompilationListener());
                } else {
                    compilationUnit = existing;
                }
            } catch (Exception e) {
                LOG.error("validateSourceCode(): ", e);
            } finally {
                doHighlighting(compilationUnit, true);
            }

            for (ICompilationError error : compilationUnit.getErrors()) {
                onCompilationError(error);
            }
            for (ICompilationError error : compilationUnit.getWarnings()) {
                onCompilationWarning(error);
            }

        } finally {
            enableDocumentListener();
            time += System.currentTimeMillis();
            System.out.println("Source code validation: " + time + " ms");
        }
    }

    protected void onSourceCodeValidation() {

    }

    protected final void doHighlighting(ICompilationUnit unit, boolean called) {
        if (unit == null) {
            throw new IllegalArgumentException("Internal error,compilation unit must not be NULL.");
        }

        if (panel == null) {
            return;
        }

        if (unit.getAST() != null) {
            onHighlightingStart();

            long time = -System.currentTimeMillis();
            try {
                final int markerCount = unit.getMarkers((String[]) null).size();
                System.out.println("DEBUG: Starting to highlight " + unit.getResource() + " with " + markerCount
                        + " markers.");
                doSemanticHighlighting(unit);
            } finally {
                time += System.currentTimeMillis();
                System.out.println("DEBUG: Highlighting " + unit.getResource() + " took " + time + " ms.");
            }
        }

        if (unit.hasErrors()) {
            highlightCompilationErrors(compilationUnit);
        }
    }

    protected void onHighlightingStart() {
    }

    protected final void doSemanticHighlighting(ICompilationUnit unit) {
        if (unit.getAST() == null) {
            return;
        }

        // changing character styles triggers
        // change events that in turn would
        // again trigger recompilation...we don't want that...
        disableDocumentListener();

        try {
            final ITextRegion visible = getVisibleTextRegion();
            if (visible != null) {
                final List<ASTNode> nodes = unit.getAST().getNodesInRange(visible);
                System.out.println("Highlighting " + nodes.size() + " nodes.");
                for (ASTNode child : nodes) {
                    doSemanticHighlighting(unit, child);
                }
            }
        } finally {
            enableDocumentListener();
        }
    }

    protected final void doSemanticHighlighting(ICompilationUnit unit, ASTNode node) {
        if (highlight(node)) {
            return; // don't highlight children if parent already was
        }

        if (!(node instanceof IncludeSourceFileNode)) {
            for (ASTNode child : node.getChildren()) {
                doSemanticHighlighting(unit, child);
            }
        }
    }

    protected final boolean highlight(ASTNode node) {
        if (node instanceof StartMacroNode || node instanceof EndMacroNode) {
            highlight(node, macroDefinitionStyle);
            return true;
        } else if (node instanceof InvokeMacroNode) {
            highlight(node, macroInvocationStyle);
            return true;
        } else if (node instanceof InstructionNode) {
            ITextRegion children = null;
            for (ASTNode child : node.getChildren()) {
                if (children == null) {
                    children = child.getTextRegion();
                } else {
                    children.merge(child.getTextRegion());
                }
            }
            ITextRegion whole = new TextRegion(node.getTextRegion());
            whole.subtract(children);
            highlight(whole, instructionStyle);
            return true;
        } else if (node instanceof IPreprocessorDirective) {
            highlight(node, preProcessorStyle);
            return true;
        } else if (node instanceof SymbolReferenceNode || node instanceof LabelNode) {
            highlight(node, labelStyle);
            return true;
        } else if (node instanceof CommentNode) {
            highlight(node, commentStyle);
            return true;
        } else if (node instanceof RegisterReferenceNode) {
            highlight(node, registerStyle);
            return true;
        }
        return false;
    }

    protected final void highlight(ASTNode node, AttributeSet attributes) {
        highlight(node.getTextRegion(), attributes);
    }

    protected final void highlight(ITextRegion range, AttributeSet attributes) {
        editorPane.getStyledDocument().setCharacterAttributes(range.getStartingOffset(), range.getLength(),
                attributes, true);
    }

    public final void moveCursorTo(ITextRegion location, boolean updateNavigationHistory) {
        moveCursorTo(location.getStartingOffset(), updateNavigationHistory);
    }

    public final void moveCursorTo(int offset, boolean updateNavigationHistory) {
        if (compilationUnit == null || compilationUnit.getAST() == null) {
            return;
        }

        if (!editorPane.hasFocus()) {
            editorPane.requestFocus();
        }
        try {
            if (!updateNavigationHistory) {
                disableNavigationHistoryUpdates();
                try {
                    editorPane.setCaretPosition(offset);
                } finally {
                    enableNavigationHistoryUpdates();
                }
            } else {
                editorPane.setCaretPosition(offset);
            }
        } catch (IllegalArgumentException e) {
            LOG.error("moveCursorTo(): Failed to offset " + offset + " on project " + project + " , resource "
                    + sourceInMemory, e);
        }
        centerCurrentLineInScrollPane();
    }

    public final void centerCurrentLineInScrollPane() {
        final Runnable r = new Runnable() {
            @Override
            public void run() {

                final Container container = SwingUtilities.getAncestorOfClass(JViewport.class, editorPane);

                if (container == null) {
                    return;
                }

                try {
                    final Rectangle r = editorPane.modelToView(editorPane.getCaretPosition());
                    if (r == null) {
                        return;
                    }
                    final JViewport viewport = (JViewport) container;
                    final int extentHeight = viewport.getExtentSize().height;
                    final int viewHeight = viewport.getViewSize().height;

                    int y = Math.max(0, r.y - (extentHeight / 2));
                    y = Math.min(y, viewHeight - extentHeight);

                    viewport.setViewPosition(new Point(0, y));
                } catch (BadLocationException ble) {
                    LOG.error("centerCurrentLineInScrollPane(): ", ble);
                }
            }

        };

        UIUtils.invokeLater(r);
    }

    protected final boolean canNavigationHistoryBack() {
        return navigationHistory.canGoBack();
    }

    protected final boolean canNavigationHistoryForward() {
        return navigationHistory.canGoForward();
    }

    protected final void navigationHistoryBack() {
        gotoLocation(navigationHistory.goBack());
    }

    protected final void navigationHistoryForward() {
        gotoLocation(navigationHistory.goForward());
    }

    // guaranteed to only be called on EDT
    protected void onNavigationHistoryChange() {

    }

    public final void gotoLocation(final int offset) {
        gotoLocation(project, sourceInMemory, offset, false);
    }

    private final void gotoLocation(Location location) {
        if (location == null) {
            return;
        }
        IAssemblyProject project = workspace.getProjectForResource(location.getResource());
        gotoLocation(project, location.getResource(), location.getOffset(), true);
    }

    private final void gotoLocation(IAssemblyProject project, IResource resource, final int offset,
            final boolean triggeredProgramatically) {
        if (this.project != project || !isCurrentlyDisplayed(resource)) {
            if (getViewContainer() instanceof EditorContainer) {
                try {
                    ((EditorContainer) getViewContainer()).openResource(workspace, project, resource, offset);
                } catch (IOException e) {
                    LOG.error("gotoLocation(): Failed for project " + project + ", resource " + resource, e);
                }
                return;
            }

            try {
                openResource(project, resource, offset, true);
            } catch (IOException e) {
                LOG.error("gotoLocation(): Failed to project " + project + ",resource " + resource, e);
            }
            return;
        }

        final Runnable r = new Runnable() {
            @Override
            public void run() {
                editorPane.requestFocusInWindow();

                if (triggeredProgramatically) {
                    disableNavigationHistoryUpdates();
                    editorPane.setCaretPosition(offset);
                    enableNavigationHistoryUpdates();
                } else {
                    editorPane.setCaretPosition(offset);
                }
                centerCurrentLineInScrollPane();
            }
        };

        UIUtils.invokeLater(r);
    }

    protected final boolean isCurrentlyDisplayed(IResource resource) {
        if (this.sourceInMemory == null) {
            return false;
        }

        return this.sourceInMemory.getIdentifier().equals(resource.getIdentifier());
    }

    protected final void disableNavigationHistoryUpdates() {
        navigationHistoryUpdatesEnabled = false;
    }

    protected final void enableNavigationHistoryUpdates() {
        navigationHistoryUpdatesEnabled = true;
    }

    protected final ITextRegion getVisibleTextRegion() {
        JViewport viewport = editorScrollPane.getViewport();
        Rectangle viewRect = viewport.getViewRect();

        Point p1 = viewRect.getLocation();
        int startIndex = editorPane.viewToModel(p1);
        if (startIndex < 0) {
            return null;
        }
        Point p2 = new Point(p1.x + viewRect.width - 10, p1.y + viewRect.height - 10); // -10 is some arbitrary offset to fix an issue with viewToModel() returning a position at the end of the input text 
        int endIndex = editorPane.viewToModel(p2);
        if (endIndex < 0) {
            return null;
        }

        int len = endIndex - startIndex;
        if (len < 0) {
            return null;
        }
        System.out.println("getVisibleTextRegion( " + p1 + " , " + p2 + " => " + startIndex + "," + endIndex);
        return new TextRegion(startIndex, len);
    }

    //   protected final ITextRegion getVisibleTextRegion() 
    //   {
    //      final Point startPoint = editorScrollPane.getViewport().getViewPosition();
    //      final Dimension size = editorScrollPane.getViewport().getExtentSize();
    //
    //      final Point endPoint = new Point(startPoint.x + size.width, startPoint.y + size.height);
    //      try {
    //         final int start = editorPane.viewToModel( startPoint );
    //         if ( start < 0 ) {
    //            return null;
    //         }
    //         final int end = editorPane.viewToModel( endPoint );
    //         if ( end < 0 ) {
    //            return null;
    //         }
    //         final int len = end-start;
    //         if ( len < 0  ) {
    //            return null;
    //         }
    //         return new TextRegion( start , len );
    //      } 
    //      catch(NullPointerException e) 
    //      {
    //         LOG.error("getVisibleTextRegion(): Caught ",e);
    //         return null;
    //      }
    //   }

    protected final void clearCompilationErrors() {
        disableDocumentListener();
        try {
            final StyledDocument doc = editorPane.getStyledDocument();
            doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, true);
        } finally {
            enableDocumentListener();
        }
    }

    protected final void disableDocumentListener() {
        documentListenerDisableCount++;
        editorPane.getDocument().removeDocumentListener(recompilationListener);
        if (editorPane.getDocument() instanceof AbstractDocument) {
            ((AbstractDocument) editorPane.getDocument()).setDocumentFilter(null);
        }
    }

    protected final void enableDocumentListener() {
        documentListenerDisableCount--;
        if (documentListenerDisableCount == 0) {
            editorPane.getDocument().addDocumentListener(recompilationListener);
            if (editorPane.getDocument() instanceof AbstractDocument) {
                ((AbstractDocument) editorPane.getDocument()).setDocumentFilter(documentFilter);
            }
        }
    }

    protected final void highlightCompilationErrors(ICompilationUnit unit) {
        disableDocumentListener();

        try {
            for (ICompilationError error : unit.getErrors()) {
                final ITextRegion location;
                if (error.getLocation() != null) {
                    location = error.getLocation();
                } else {
                    if (error.getErrorOffset() != -1) {
                        location = new TextRegion(error.getErrorOffset(), 1);
                    } else {
                        location = null;
                    }
                }

                if (location != null) {
                    highlight(location, errorStyle);
                }
            }
        } finally {
            enableDocumentListener();
        }
    }

    protected void onCompilationError(ICompilationError error) {

    }

    protected void onCompilationWarning(ICompilationError error) {

    }

    // ============= view creation ===================

    @Override
    public JPanel getPanel() {
        if (panel == null) {
            panel = createPanel();
            if (this.persistentResource != null) {
                try {
                    validateSourceCode();
                } catch (IOException e) {
                    LOG.error("getPanel(): ", e);
                }
            }
        }
        return panel;
    }

    private final MouseListener mouseListener = new MouseAdapter() {

        public void mouseClicked(java.awt.event.MouseEvent e) {

            if (e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON1
                    && (e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) {
                // navigate to symbol definition
                final ASTNode node = getASTNodeForLocation(e.getPoint());
                if (node instanceof SymbolReferenceNode) {
                    final SymbolReferenceNode ref = (SymbolReferenceNode) node;
                    gotoToSymbolDefinition(ref);
                }
            }
        }
    };

    protected final void gotoToSymbolDefinition(SymbolReferenceNode ref) {
        if (getCurrentCompilationUnit() != null) {
            ISymbolTable table = getCurrentCompilationUnit().getSymbolTable();
            ISymbol symbol = ref.resolve(table, true);
            if (symbol != null) {
                if (symbol != null) {
                    final ITextRegion location = symbol.getLocation();
                    final ICompilationUnit newCompilationUnit = symbol.getCompilationUnit();
                    final IAssemblyProject project = workspace
                            .getProjectForResource(newCompilationUnit.getResource());
                    gotoLocation(project, newCompilationUnit.getResource(), location.getStartingOffset(), false);
                }
            }
        }
    }

    private final JPanel createPanel() {
        disableDocumentListener(); // necessary because setting colors on editor pane triggers document change listeners (is considered a style change...)

        try {
            editorPane.setEditable(editable);
            editorPane.getDocument().addUndoableEditListener(undoListener);
            editorPane.setCaretColor(Color.WHITE);
            setupKeyBindings(editorPane);
            setColors(editorPane);
            editorScrollPane = new JScrollPane(editorPane);
            setColors(editorScrollPane);
            editorPane.addCaretListener(listener);
            editorPane.addMouseListener(mouseListener);
            editorPane.addMouseMotionListener(new MouseMotionAdapter() {

                @Override
                public void mouseMoved(MouseEvent e) {
                    if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) != 0) // ctrl pressed
                    {
                        final ASTNode node = getASTNodeForLocation(e.getPoint());
                        if (node instanceof SymbolReferenceNode) {
                            maybeUnderlineIdentifierAt(e.getPoint());
                        } else {
                            clearUnderlineHighlight();
                        }
                    } else if (compilationUnit != null) {
                        String tooltipText = null;
                        if (compilationUnit != null) {
                            final ASTNode node = getASTNodeForLocation(e.getPoint());
                            if (node instanceof InvokeMacroNode) {
                                tooltipText = ExpandMacrosPhase.expandInvocation((InvokeMacroNode) node,
                                        compilationUnit);
                                if (tooltipText != null) {
                                    tooltipText = "<html>" + tooltipText.replace("\n", "<br>") + "</html>";
                                }
                            }
                        }
                        if (!StringUtils.equals(editorPane.getToolTipText(), tooltipText)) {
                            editorPane.setToolTipText(tooltipText);
                        }
                    }
                }
            });
            editorPane.addMouseListener(popupListener);
        } finally {
            enableDocumentListener();
        }

        EditorContainer.addEditorCloseKeyListener(editorPane, this);

        editorScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        editorScrollPane.setPreferredSize(new Dimension(400, 600));
        editorScrollPane.setMinimumSize(new Dimension(100, 100));

        final AdjustmentListener adjustmentListener = new AdjustmentListener() {

            @Override
            public void adjustmentValueChanged(AdjustmentEvent e) {
                if (!e.getValueIsAdjusting()) {
                    if (compilationUnit != null) {
                        doHighlighting(compilationUnit, false);
                    }
                }
            }
        };
        editorScrollPane.getVerticalScrollBar().addAdjustmentListener(adjustmentListener);
        editorScrollPane.getHorizontalScrollBar().addAdjustmentListener(adjustmentListener);

        // button panel
        final JPanel topPanel = new JPanel();

        final JToolBar toolbar = new JToolBar();

        setColors(toolbar);

        cursorPosition.setSize(new Dimension(400, 15));
        cursorPosition.setEditable(false);
        setColors(cursorPosition);

        /**
         * TOOLBAR
         * SOURCE
         * cursor position
         * status area 
         */
        topPanel.setLayout(new GridBagLayout());

        GridBagConstraints cnstrs = constraints(0, 0, GridBagConstraints.HORIZONTAL);
        cnstrs.gridwidth = GridBagConstraints.REMAINDER;
        cnstrs.weighty = 0;
        topPanel.add(toolbar, cnstrs);

        cnstrs = constraints(0, 1, GridBagConstraints.BOTH);
        cnstrs.gridwidth = GridBagConstraints.REMAINDER;
        topPanel.add(editorScrollPane, cnstrs);

        cnstrs = constraints(0, 2, GridBagConstraints.HORIZONTAL);
        cnstrs.gridwidth = GridBagConstraints.REMAINDER;
        cnstrs.weighty = 0;
        topPanel.add(cursorPosition, cnstrs);

        cnstrs = constraints(0, 3, GridBagConstraints.HORIZONTAL);
        cnstrs.gridwidth = GridBagConstraints.REMAINDER;
        cnstrs.weighty = 0;

        // setup result panel
        final JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        setColors(panel);
        cnstrs = constraints(0, 0, true, true, GridBagConstraints.BOTH);
        panel.add(topPanel, cnstrs);
        return panel;
    }

    /**
     * Returns the mouse pointer's location relative to the
     * editorpane or <code>null</code> if the mouse pointer is 
     * outside of the editor.
     * 
     * @return location or <code>null</code> if mouse ptr is outside of the editor pane
     */
    protected final Point getMouseLocation() {
        final Point location = MouseInfo.getPointerInfo().getLocation();
        final Point locOnScreen = editorPane.getLocationOnScreen();

        final Point result = new Point(location.x - locOnScreen.x, location.y - locOnScreen.y);
        return editorPane.contains(result) ? result : null;
    }

    protected final void setupKeyBindings(final JTextPane editor) {
        // 'Save' action 
        addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_S, Event.CTRL_MASK), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                saveCurrentFile();
            }
        });

        // 'Underline text when pressing CTRL while hovering over an identifier' 
        editorPane.addKeyListener(new KeyAdapter() {
            private boolean isControlKey(KeyEvent e) {
                return e.getKeyCode() == KeyEvent.VK_CONTROL;
            }

            public void keyPressed(KeyEvent e) {
                if (isControlKey(e)) {
                    final Point ptr = getMouseLocation();
                    if (ptr != null) {
                        maybeUnderlineIdentifierAt(ptr);
                    }
                }
            }

            public void keyReleased(KeyEvent e) {
                if (isControlKey(e)) {
                    clearUnderlineHighlight();
                }
            };
        });

        // "Undo" action
        addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_Z, Event.CTRL_MASK), undoAction);

        addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_Y, Event.CTRL_MASK), redoAction);

        // 'Search' action 
        addKeyBinding(editor, KeyStroke.getKeyStroke(KeyEvent.VK_F, Event.CTRL_MASK), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                showSearchDialog();
            }
        });

        setupKeyBindingsHook(editor);
    }

    protected void setupKeyBindingsHook(JTextPane editor) {

    }

    protected final void saveCurrentFile() {

        if (!hasUnsavedContent()) {
            return;
        }

        final String source = getTextFromTextPane();
        try {
            Misc.writeResource(getCurrentResource(), source);
            this.initialHashCode = Misc.calcHash(source);
            updateTitle();
        } catch (IOException e1) {
            LOG.error("save(): Failed to write to " + getCurrentResource());
            return;
        }

        if (compilationUnit == null || compilationUnit.hasErrors()) {
            return;
        }

        try {
            getCurrentProject().getProjectBuilder().build();
        } catch (IOException e) {
            LOG.error("save(): Compilation failed", e);
        }
    }

    public final IAssemblyProject getCurrentProject() {
        return project;
    }

    public final IResource getCurrentResource() {
        return this.persistentResource;
    }

    public final IResource getSourceFromMemory() {
        return this.sourceInMemory;
    }

    @Override
    public final void disposeHook() {
        try {
            disposeHook2();
        } finally {
            if (registeredWithTooltipManager) {
                ToolTipManager.sharedInstance().unregisterComponent(editorPane);
            }
            workspace.removeWorkspaceListener(workspaceListener);
            navigationHistory.removeListener(navigationHistoryListener);
        }
    }

    protected void disposeHook2() {
    }

    @Override
    public final void refreshDisplay() {
        try {
            if (project != null) {
                validateSourceCode();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        refreshDisplayHook();
    }

    protected void refreshDisplayHook() {

    }

    @Override
    public String getTitle() {
        if (getCurrentResource() == null) {
            return "source view";
        }
        final String prefix = hasUnsavedContent() ? "*" : "";
        final String identifier;
        if (getCurrentResource() instanceof FileResource) {
            identifier = ((FileResource) getCurrentResource()).getFile().getName();
        } else {
            identifier = getCurrentResource().getIdentifier();
        }
        return prefix + identifier;
    }

    @Override
    public final boolean hasUnsavedContent() {
        if (this.persistentResource == null) {
            return false;
        }
        return !initialHashCode.equals(Misc.calcHash(getTextFromTextPane()));
    }

    @Override
    public final boolean mayBeDisposed() {
        return !hasUnsavedContent();
    }

    protected final void updateTitle() {
        final String title = (hasUnsavedContent() ? "*" : "") + getCurrentResource().getIdentifier();
        getViewContainer().setTitle(SourceCodeView.this, title);
    }

    @Override
    public String getID() {
        return "source-view";
    }

    public final boolean isEditable() {
        return editable;
    }

    public void setEditable(boolean editable) {
        this.editable = editable;
        editorPane.setEditable(editable);
    }

    protected final ICompilationUnit getCurrentCompilationUnit() {
        return compilationUnit;
    }

    protected final void addMouseListener(MouseAdapter listener) {
        editorPane.addMouseListener(listener);
        editorPane.addMouseMotionListener(listener);
    }

    protected final void showTooltip(String s) {
        if (!registeredWithTooltipManager) {
            ToolTipManager.sharedInstance().registerComponent(editorPane);
            registeredWithTooltipManager = true;
        }
        editorPane.setToolTipText(s);
    }

    protected final void clearTooltip() {
        editorPane.setToolTipText(null);
    }

    protected final void removeMouseListener(MouseAdapter listener) {
        editorPane.removeMouseListener(listener);
        editorPane.removeMouseMotionListener(listener);
    }

    protected final int getModelOffsetForLocation(Point p) {
        return editorPane.viewToModel(p);
    }

    protected final void addPopupMenu(PopupMenu menu) {
        editorPane.add(menu);
    }

    protected final ASTNode getASTNodeForLocation(Point p) {
        final AST ast = getCurrentCompilationUnit() != null ? getCurrentCompilationUnit().getAST() : null;
        if (ast == null) {
            return null;
        }

        int offset = editorPane.viewToModel(p);
        if (offset != -1) {
            return ast.getNodeInRange(offset);
        }
        return null;
    }

    protected class PopupListener extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            maybeShowPopup(e);
        }

        public void mouseReleased(MouseEvent e) {
            maybeShowPopup(e);
        }

        private void maybeShowPopup(MouseEvent e) {
            if (e.isPopupTrigger()) {
                final ASTNode node = getASTNodeForLocation(e.getPoint());
                final JPopupMenu menu = createPopupMenu(node, editorPane.getCaretPosition(),
                        editorPane.getSelectedText());
                if (menu != null) {
                    menu.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        }
    }

    protected JPopupMenu createPopupMenu(ASTNode node, int caretPosition, String currentSelection) {
        return null;
    }

    protected final void showSearchDialog() {
        final int cursorPos = editorPane.getCaretPosition();
        final String selection = editorPane.getSelectedText();
        if (searchDialog == null) {
            searchDialog = new SearchDialog();
            searchDialog.setVisible(true);
            searchDialog.activate(selection, cursorPos);
        } else {
            searchDialog.activate(selection, cursorPos);
        }
    }

    protected final void highlightLocation(ITextRegion region) {

        if (region == null) {
            throw new IllegalArgumentException("region must not be null");
        }

        try {
            if (currentHighlight == null) {
                currentHighlight = editorPane.getHighlighter().addHighlight(region.getStartingOffset(),
                        region.getEndOffset(), new DefaultHighlighter.DefaultHighlightPainter(Color.WHITE));
            } else {
                editorPane.getHighlighter().changeHighlight(currentHighlight, region.getStartingOffset(),
                        region.getEndOffset());
            }
        } catch (BadLocationException e) {
            LOG.error("highlightLocation(): Bad location " + region, e);
            throw new RuntimeException("Bad text location " + region, e);
        }
    }

    protected final void clearUnderlineHighlight() {
        final Runnable r = new Runnable() {

            @Override
            public void run() {
                if (currentUnderlineHighlight != null) {
                    editorPane.getHighlighter().removeHighlight(currentUnderlineHighlight);
                    currentUnderlineHighlight = null;
                    editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                    editorPane.repaint();
                }
            }
        };
        UIUtils.invokeLater(r);
    }

    protected final void maybeUnderlineIdentifierAt(Point mouseLocation) {

        final ASTNode node = getASTNodeForLocation(mouseLocation);
        if (!(node instanceof SymbolReferenceNode)) {
            return;
        }
        final SymbolReferenceNode ref = (SymbolReferenceNode) node;
        underlineLocation(ref.getTextRegion());
    }

    protected final void underlineLocation(final ITextRegion region) {
        if (region == null) {
            throw new IllegalArgumentException("region must not be NULL.");
        }

        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    if (currentUnderlineHighlight == null) {
                        currentUnderlineHighlight = editorPane.getHighlighter().addHighlight(
                                region.getStartingOffset(), region.getEndOffset(),
                                new UnderlineHighlightPainter(Color.BLUE, 1));
                    } else {
                        editorPane.getHighlighter().changeHighlight(currentUnderlineHighlight,
                                region.getStartingOffset(), region.getEndOffset());
                    }
                    editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                    editorPane.repaint();
                } catch (BadLocationException e) {
                    LOG.error("underlineLocation(): Bad location " + region, e);
                }
            }
        };
        UIUtils.invokeLater(r);
    }

    protected final void clearHighlight() {
        if (currentHighlight != null) {
            editorPane.getHighlighter().removeHighlight(currentHighlight);
            currentHighlight = null;
        }
    }

    protected static enum Direction {
        FORWARD {

            @Override
            public int advance(int index) {
                return index + 1;
            }
        },
        BACKWARD {

            @Override
            public int advance(int index) {
                return index - 1;
            }
        };

        public abstract int advance(int index);
    }

    protected final class SearchDialog extends JFrame {

        private final JTextField searchPattern = new JTextField();

        private final JCheckBox wrapSearch = new JCheckBox("Wrap", false);
        private final JCheckBox caseSensitive = new JCheckBox("Match case", false);

        private final JButton nextButton = new JButton("Next");
        private final JButton previousButton = new JButton("Previous");
        private final JButton closeButton = new JButton("Close");

        private final JTextField messageArea = new JTextField("", 25);

        private String lastSearchPattern;
        private int lastMatch = -1;
        private int currentIndex = 0;
        private Direction lastDirection = Direction.FORWARD;

        public SearchDialog() {
            super("Search");

            messageArea.setBackground(null);
            messageArea.setEditable(false);
            messageArea.setBorder(null);
            messageArea.setFocusable(false);

            setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

            final JPanel panel = new JPanel();
            panel.setLayout(new GridBagLayout());

            // add search pattern
            GridBagConstraints cnstrs = constraints(0, 0, true, false, GridBagConstraints.HORIZONTAL);
            panel.add(searchPattern, cnstrs);

            searchPattern.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    search(lastDirection);
                }
            });

            // add wrap checkbox
            cnstrs = constraints(0, 1, false, false, GridBagConstraints.HORIZONTAL);
            panel.add(wrapSearch, cnstrs);

            // 'case-sensitive' checkbox
            cnstrs = constraints(1, 1, false, false, GridBagConstraints.HORIZONTAL);
            panel.add(caseSensitive, cnstrs);

            // add message area
            cnstrs = constraints(0, 2, true, false, GridBagConstraints.HORIZONTAL);
            panel.add(messageArea, cnstrs);

            // create button panel
            final JPanel buttonPanel = new JPanel();
            buttonPanel.setLayout(new GridBagLayout());

            cnstrs = constraints(0, 0, false, true, GridBagConstraints.HORIZONTAL);
            buttonPanel.add(previousButton, cnstrs);
            previousButton.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    search(Direction.BACKWARD);
                }
            });

            cnstrs = constraints(1, 0, false, true, GridBagConstraints.HORIZONTAL);
            buttonPanel.add(nextButton, cnstrs);
            nextButton.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    search(Direction.FORWARD);
                }
            });

            cnstrs = constraints(2, 0, true, true, GridBagConstraints.HORIZONTAL);
            buttonPanel.add(closeButton, cnstrs);
            closeButton.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    setVisible(false);
                    clearHighlight();
                }
            });

            // add button panel
            // add wrap checkbox
            cnstrs = constraints(0, 3, true, true, GridBagConstraints.HORIZONTAL);
            panel.add(buttonPanel, cnstrs);

            // add everything to content pane
            getContentPane().add(panel);
            setAlwaysOnTop(true);
            pack();
        }

        public final void activate(String selectedText, int cursorPos) {
            setVisible(true);

            currentIndex = cursorPos;
            final String text = StringUtils.isNotBlank(selectedText) ? selectedText : lastSearchPattern;
            if (text != null) {
                searchPattern.setText(selectedText);
            }
            searchPattern.requestFocus();
        }

        protected void search(Direction direction) {

            showMessage(null);

            final String source = getTextFromTextPane();
            final String pattern = searchPattern.getText();

            if (StringUtils.isBlank(pattern)) {
                showMessage("Please enter a search pattern");
                return;
            }
            lastSearchPattern = pattern;

            lastDirection = direction;

            if (lastMatch != -1) // advance past last match 
            {
                if (!advance(source, direction, pattern, lastMatch)) {
                    clearHighlight();
                    showNotFoundMessage();
                    return;
                }
            }

            // remember start index so we don't loop forever
            // if the search pattern doesn't match at all
            final int searchStartIndex = currentIndex;
            final boolean matchCaseSensitive = caseSensitive.isSelected();
            do {
                final String currentText = source.substring(currentIndex, currentIndex + pattern.length());

                final boolean matches;
                if (matchCaseSensitive) {
                    matches = currentText.equals(pattern);
                } else {
                    matches = currentText.equalsIgnoreCase(pattern);
                }

                if (matches) {
                    lastMatch = currentIndex;
                    gotoLocation(currentIndex);
                    editorPane.requestFocus();
                    editorPane.setCaretPosition(currentIndex);
                    highlightLocation(new TextRegion(currentIndex, pattern.length()));
                    // TODO: Maybe show 'match found' message ?
                    return;
                }
                if (!advance(source, direction, pattern, currentIndex)) {
                    clearHighlight();
                    showNotFoundMessage();
                    break;
                }
            } while (currentIndex != searchStartIndex);

            if (lastMatch != -1) {
                currentIndex = lastMatch;
            }
            clearHighlight();
            showNotFoundMessage();
        }

        private void showNotFoundMessage() {
            showMessage("No (more) matches");
        }

        private boolean advance(String source, Direction direction, String pattern, int index) {
            int newIndex = direction.advance(index);
            if (newIndex < 0) {
                if (wrapSearch.isSelected()) {
                    showSearchWrappedMessage();
                    newIndex = source.length() - 1 - pattern.length();
                } else {
                    return false;
                }
            } else if ((newIndex + pattern.length()) >= source.length()) {
                if (wrapSearch.isSelected()) {
                    showSearchWrappedMessage();
                    newIndex = 0;
                } else {
                    return false;
                }
            }
            currentIndex = newIndex;
            return true;
        }

        private void showSearchWrappedMessage() {
            showMessage("Search wrapped");
        }

        private void showMessage(String message) {
            messageArea.setText(message);
        }
    }

    protected final int getCaretPosition() {
        return editorPane.getCaretPosition();
    }
}